c# - puente - la franela fue tan bueno
¿Qué interfaces fluidas has creado o visto en C#que fueron muy valiosas? ¿Qué fue tan bueno de ellos? (11)
Además de los que se especifican aquí, el marco simulado de prueba unitaria RhinoMocks RhinoMocks utiliza una sintaxis fluida para especificar las expectativas sobre los objetos simulados:
// Expect mock.FooBar method to be called with any paramter and have it invoke some method
Expect.Call(() => mock.FooBar(null))
.IgnoreArguments()
.WhenCalled(someCallbackHere);
// Tell mock.Baz property to return 5:
SetupResult.For(mock.Baz).Return(5);
Las "interfaces fluidas" son un tema bastante candente en estos días. C # 3.0 tiene algunas características interesantes (particularmente métodos de extensión) que te ayudan a hacerlas.
Para su información, una API fluida significa que cada llamada de método devuelve algo útil, a menudo el mismo objeto al que llamó el método, para que pueda seguir encadenando cosas. Martin Fowler lo discute con un ejemplo de Java here . El concepto kooks algo como esto:
var myListOfPeople = new List<Person>();
var person = new Person();
person.SetFirstName("Douglas").SetLastName("Adams").SetAge(42).AddToList(myListOfPeople);
He visto algunas interfaces fluidas increíblemente útiles en C # (un ejemplo es el enfoque fluido para validar parámetros encontrados en una pregunta anterior de StackOverflow que había preguntado . Me sorprendió. Fue capaz de proporcionar una sintaxis muy legible para expresar reglas de validación de parámetros, y Además, si no hubiera excepciones, ¡fue capaz de evitar la creación de instancias de objetos! Entonces, para el "caso normal", había muy poca sobrecarga. Este dato me enseñó una gran cantidad en poco tiempo. Quiero encontrar más cosas. como eso).
Entonces, me gustaría aprender más observando y discutiendo algunos ejemplos excelentes. Entonces, ¿cuáles son algunas excelentes interfaces fluidas que has creado o visto en C #, y qué las hizo tan valiosas?
Gracias.
Aquí hay uno que hice ayer. Pensar más a fondo puede llevarme a cambiar el enfoque, pero incluso si es así, el enfoque "fluido" me permite lograr algo que de otra manera no podría tener.
En primer lugar, algunos antecedentes. Recientemente aprendí (aquí en ) una forma de pasar un valor a un método de tal manera que el método podría determinar tanto el nombre como el valor . Por ejemplo, un uso común es para la validación de parámetros. Por ejemplo:
public void SomeMethod(Invoice lastMonthsInvoice)
{
Helper.MustNotBeNull( ()=> lastMonthsInvoice);
}
Tenga en cuenta que no hay una cadena que contenga "lastMonthsInvoice", lo cual es bueno porque las cadenas apestan para refactorizar. Sin embargo, el mensaje de error puede decir algo como "El parámetro ''lastMonthsInvoice'' no debe ser nulo". Aquí está la publicación que explica por qué esto funciona y apunta a la publicación del blog del chico.
Pero eso es sólo el fondo. Estoy usando el mismo concepto, pero de una manera diferente. Estoy escribiendo algunas pruebas de unidad, y quiero descargar ciertos valores de propiedad a la consola para que aparezcan en la salida de prueba de la unidad. Me cansé de escribir esto:
Console.WriteLine("The property ''lastMonthsInvoice'' has the value: " + lastMonthsInvoice.ToString());
... porque tengo que nombrar la propiedad como una cadena y luego referirme a ella. Así que lo hice donde podría escribir esto:
ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice );
Y consigue esta salida:
Property [lastMonthsInvoice] is: <whatever ToString from Invoice
produce>
Ahora, aquí es donde un enfoque fluido me permitió hacer algo que de otra manera no podría hacer.
Quería hacer que ConsoleHelper.WriteProperty tomara una matriz params, para que pudiera volcar muchos de esos valores de propiedad en la consola. Para ello, su firma se vería así:
public static void WriteProperty<T>(params Expression<Func<T>>[] expr)
Así que podría hacer esto:
ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice, ()=> firstName, ()=> lastName );
Sin embargo, eso no funciona debido a la inferencia de tipos. En otras palabras, todas estas expresiones no devuelven el mismo tipo. lastMonthsInvoice es una factura. FirstName y lastName son cadenas. No se pueden usar en la misma llamada a WriteProperty, porque T no es lo mismo en todos ellos.
Aquí es donde el enfoque fluido vino al rescate. Hice WriteProperty () devolver algo. El tipo que devolvió es algo a lo que puedo llamar And (). Esto me da esta sintaxis:
ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice)
.And( ()=> firstName)
.And( ()=> lastName);
Este es un caso en el que el enfoque fluido permitía algo que de otro modo no habría sido posible (o al menos no conveniente).
Aquí está la implementación completa. Como dije, lo escribí ayer. Probablemente veas un margen de mejora o incluso mejores enfoques. Doy la bienvenida a eso.
public static class ConsoleHelper
{
// code where idea came from ...
//public static void IsNotNull<T>(Expression<Func<T>> expr)
//{
// // expression value != default of T
// if (!expr.Compile()().Equals(default(T)))
// return;
// var param = (MemberExpression)expr.Body;
// throw new ArgumentNullException(param.Member.Name);
//}
public static PropertyWriter WriteProperty<T>(Expression<Func<T>> expr)
{
var param = (MemberExpression)expr.Body;
Console.WriteLine("Property [" + param.Member.Name + "] = " + expr.Compile()());
return null;
}
public static PropertyWriter And<T>(this PropertyWriter ignored, Expression<Func<T>> expr)
{
ConsoleHelper.WriteProperty(expr);
return null;
}
public static void Blank(this PropertyWriter ignored)
{
Console.WriteLine();
}
}
public class PropertyWriter
{
/// <summary>
/// It is not even possible to instantiate this class. It exists solely for hanging extension methods off.
/// </summary>
private PropertyWriter() { }
}
Como @ mencionó, Ninject usa este tipo de API para especificar enlaces. Aquí hay algunos ejemplos de código de su guía de usuario :
Bind<IWeapon>().To<Sword>();
Bind<Samurai>().ToSelf();
Bind<Shogun>().ToSelf().Using<SingletonBehavior>();
El nuevo HttpClient del WCF REST Starter Kit Preview 2 es una excelente API fluida. ver mi blog para una muestra http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/
Escribí un pequeño paquete fluido para System.Net. El correo electrónico que encuentro hace que el código del correo electrónico sea mucho más legible (y más fácil de recordar la sintaxis).
var email = Email
.From("[email protected]")
.To("[email protected]", "bob")
.Subject("hows it going bob")
.Body("yo dawg, sup?");
//send normally
email.Send();
//send asynchronously
email.SendAsync(MailDeliveredCallback);
Esta es realmente la primera vez que escucho el término "interfaz fluida". Pero los dos ejemplos que vienen a la mente son LINQ y colecciones inmutables.
Bajo las cubiertas, LINQ es una serie de métodos, la mayoría de los cuales son métodos de extensión, que toman al menos un IEnumerable y devuelven otro IEnumerable. Esto permite un encadenamiento de métodos muy potente.
var query = someCollection.Where(x => !x.IsBad).Select(x => x.Property1);
Tipos inmutables, y más específicamente las colecciones tienen un patrón muy similar. Las Colecciones inmutables devuelven una nueva colección para lo que normalmente sería una operación de mutación. Así que la creación de una colección a menudo se convierte en una serie de llamadas a métodos encadenados.
var array = ImmutableCollection<int>.Empty.Add(42).Add(13).Add(12);
Felicitaciones por la validación de parámetros del método, me ha dado una nueva idea para nuestras API fluidas. De todas formas he odiado nuestros controles de condición previa ...
He construido un sistema de extensibilidad para un nuevo producto en desarrollo, donde puede describir con fluidez los comandos disponibles, los elementos de la interfaz de usuario y más. Esto se ejecuta sobre StructureMap y FluentNHibernate, que también son buenas API.
MenuBarController mb;
// ...
mb.Add(Resources.FileMenu, x =>
{
x.Executes(CommandNames.File);
x.Menu
.AddButton(Resources.FileNewCommandImage, Resources.FileNew, Resources.FileNewTip, y => y.Executes(CommandNames.FileNew))
.AddButton(null, Resources.FileOpen, Resources.FileOpenTip, y =>
{
y.Executes(CommandNames.FileOpen);
y.Menu
.AddButton(Resources.FileOpenFileCommandImage, Resources.OpenFromFile, Resources.OpenFromFileTop, z => z.Executes(CommandNames.FileOpenFile))
.AddButton(Resources.FileOpenRecordCommandImage, Resources.OpenRecord, Resources.OpenRecordTip, z => z.Executes(CommandNames.FileOpenRecord));
})
.AddSeperator()
.AddButton(null, Resources.FileClose, Resources.FileCloseTip, y => y.Executes(CommandNames.FileClose))
.AddSeperator();
// ...
});
Y puedes configurar todos los comandos disponibles así:
Command(CommandNames.File)
.Is<DummyCommand>()
.AlwaysEnabled();
Command(CommandNames.FileNew)
.Bind(Shortcut.CtrlN)
.Is<FileNewCommand>()
.Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);
Command(CommandNames.FileSave)
.Bind(Shortcut.CtrlS)
.Enable(WorkspaceStatusProviderNames.DocumentOpen)
.Is<FileSaveCommand>();
Command(CommandNames.FileSaveAs)
.Bind(Shortcut.CtrlShiftS)
.Enable(WorkspaceStatusProviderNames.DocumentOpen)
.Is<FileSaveAsCommand>();
Command(CommandNames.FileOpen)
.Is<FileOpenCommand>()
.Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);
Command(CommandNames.FileOpenFile)
.Bind(Shortcut.CtrlO)
.Is<FileOpenFileCommand>()
.Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);
Command(CommandNames.FileOpenRecord)
.Bind(Shortcut.CtrlShiftO)
.Is<FileOpenRecordCommand>()
.Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);
Nuestra vista configura sus controles para los comandos del menú de edición estándar utilizando un servicio que les brinda el área de trabajo, donde solo le dicen que los observe:
Workspace
.Observe(control1)
.Observe(control2)
Si las fichas de usuario a los controles, el área de trabajo obtiene automáticamente un adaptador apropiado para el control y proporciona operaciones de deshacer / rehacer y del portapapeles.
Nos ha ayudado a reducir drásticamente el código de configuración y hacerlo aún más legible.
Olvidé hablar sobre una biblioteca que estamos usando en nuestros presentadores de modelos de WinForms MVP para validar las vistas: FluentValidation . ¡Realmente fácil, realmente comprobable, realmente agradable!
La API de criterios en NHibernate tiene una buena interfaz fluida que te permite hacer cosas geniales como esta:
Session.CreateCriteria(typeof(Entity))
.Add(Restrictions.Eq("EntityId", entityId))
.CreateAlias("Address", "Address")
.Add(Restrictions.Le("Address.StartDate", effectiveDate))
.Add(Restrictions.Disjunction()
.Add(Restrictions.IsNull("Address.EndDate"))
.Add(Restrictions.Ge("Address.EndDate", effectiveDate)))
.UniqueResult<Entity>();
Me encanta la interfaz fluida en CuttingEdge.Conditions .
De su muestra:
// Check all preconditions:
id.Requires("id")
.IsNotNull() // throws ArgumentNullException on failure
.IsInRange(1, 999) // ArgumentOutOfRangeException on failure
.IsNotEqualTo(128); // throws ArgumentException on failure
Descubrí que es mucho más fácil de leer y me hace mucho más eficaz para verificar mis condiciones previas (y publicar las condiciones) en los métodos que cuando tengo 50 afirmaciones para manejar las mismas comprobaciones.
SubSonic 2.1 tiene una decente para la API de consulta:
DB.Select()
.From<User>()
.Where(User.UserIdColumn).IsEqualTo(1)
.ExecuteSingle<User>();
tweetsharp también hace un uso extensivo de una API fluida:
var twitter = FluentTwitter.CreateRequest()
.Configuration.CacheUntil(2.Minutes().FromNow())
.Statuses().OnPublicTimeline().AsJson();
Y Fluido NHibernate está de moda últimamente:
public class CatMap : ClassMap<Cat>
{
public CatMap()
{
Id(x => x.Id);
Map(x => x.Name)
.WithLengthOf(16)
.Not.Nullable();
Map(x => x.Sex);
References(x => x.Mate);
HasMany(x => x.Kittens);
}
}
Ninject usa, pero no pude encontrar un ejemplo rápidamente.
Método de denominación
Las interfaces fluidas se prestan para facilitar la lectura siempre que los nombres de los métodos se elijan con sensatez.
Con eso en mente, me gustaría nominar esta API en particular como "anti-fluida":
Es un miembro de System.Type
y toma un objeto, y devuelve true si el objeto es una instancia del tipo. Desafortunadamente, naturalmente tiendes a leerlo de izquierda a derecha de esta manera:
o.IsInstanceOfType(t); // wrong
Cuando en realidad es a la inversa:
t.IsInstanceOfType(o); // right, but counter-intuitive
Pero no todos los métodos podrían ser nombrados (o posicionados en el BCL) para anticipar cómo podrían aparecer en el código "pseudo-inglés", por lo que esto no es realmente una crítica. Solo señalo otro aspecto de las interfaces fluidas: la elección de nombres de métodos para causar la menor sorpresa.
Inicializadores de objetos
Con muchos de los ejemplos que se ofrecen aquí, la única razón por la que se utiliza una interfaz fluida es para que varias propiedades de un objeto recién asignado se puedan inicializar dentro de una sola expresión.
Pero C # tiene una característica de lenguaje que muy a menudo hace que esto sea innecesario: la sintaxis del inicializador de objetos:
var myObj = new MyClass
{
SomeProperty = 5,
Another = true,
Complain = str => MessageBox.Show(str),
};
Esto quizás explique por qué los usuarios expertos de C # están menos familiarizados con el término "interfaz fluida" para encadenar llamadas en el mismo objeto, ya que no es tan necesario en C #.
Como las propiedades pueden tener configuradores codificados a mano, esta es una oportunidad para llamar a varios métodos en el objeto recién construido, sin tener que hacer que cada método devuelva el mismo objeto.
Las limitaciones son:
- Un establecedor de propiedades solo puede aceptar un argumento
- Un establecedor de propiedades no puede ser genérico
Me gustaría si pudiéramos llamar a métodos y enlistarnos en eventos, así como asignar propiedades, dentro de un bloque de inicializador de objetos.
var myObj = new MyClass
{
SomeProperty = 5,
Another = true,
Complain = str => MessageBox.Show(str),
DoSomething()
Click += (se, ev) => MessageBox.Show("Clicked!"),
};
¿Y por qué un bloque de modificaciones de este tipo solo debe aplicarse inmediatamente después de la construcción? Nosotros podríamos tener:
myObj with
{
SomeProperty = 5,
Another = true,
Complain = str => MessageBox.Show(str),
DoSomething(),
Click += (se, ev) => MessageBox.Show("Clicked!"),
}
With sería una nueva palabra clave que opera en un objeto de algún tipo y produce el mismo objeto y tipo; tenga en cuenta que esto sería una expresión , no una declaración . Así que capturaría exactamente la idea de encadenar en una "interfaz fluida".
Por lo tanto, puede usar la sintaxis de estilo inicializador independientemente de si obtuvo el objeto de una new
expresión o de un método de fábrica o IOC, etc.
De hecho, se podría usar with
después de un new
completo y sería equivalente al estilo actual del inicializador de objetos:
var myObj = new MyClass() with
{
SomeProperty = 5,
Another = true,
Complain = str => MessageBox.Show(str),
DoSomething(),
Click += (se, ev) => MessageBox.Show("Clicked!"),
};
Y como Charlie señala en los comentarios:
public static T With(this T with, Action<T> action)
{
if (with != null)
action(with);
return with;
}
La envoltura anterior simplemente obliga a una acción de no retorno a devolver algo, y listo, cualquier cosa puede ser "fluida" en ese sentido.
Equivalente al inicializador, pero con la inscripción de eventos:
var myObj = new MyClass().With(w =>
{
w.SomeProperty = 5;
w.Another = true;
w.Click += (se, ev) => MessageBox.Show("Clicked!");
};
Y en un método de fábrica en lugar de un new
:
var myObj = Factory.Alloc().With(w =>
{
w.SomeProperty = 5;
w.Another = true;
w.Click += (se, ev) => MessageBox.Show("Clicked!");
};
No pude resistirme a darle el cheque del estilo "quizás mónada" para nulo también, así que si tiene algo que podría devolver el null
, aún puede aplicar With
y luego verificar si hay null
.