2009-03-27 10 views
17

"Interfaces fluidas" es un tema bastante candente en estos días. C# 3.0 tiene algunas características agradables (particularmente métodos de extensión) que te ayudan a hacerlas.¿Qué interfaces fluidas has creado o visto en C# que fueron muy valiosas? ¿Qué fue tan bueno acerca de ellos?

Para su información, una API fluida significa que cada llamada a método devuelve algo útil, a menudo el mismo objeto sobre el que llamó el método, para que pueda seguir encadenando cosas. Martin Fowler lo analiza con un ejemplo de Java here. El concepto chiflados 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 de fluidez para la validación de los parámetros que se encuentran en an earlier StackOverflow question I had asked Lo que me impactó fue capaz de dar muy legible. sintaxis para expresar las reglas de validación de parámetros, y además, si no había excepciones, ¡era capaz de evitar la creación de instancias de cualquier objeto! Así que para el "caso normal", había muy poca sobrecarga. Este pequeño bocado me enseñó gran cantidad en un corto tiempo. Quiero encontrar más cosas así).

Entonces, me gustaría aprender más mirando y discutiendo algunos ejemplos excelentes. Entonces, ¿Cuáles son algunas de las excelentes interfaces fluidas que ha creado o visto en C#, y qué las hizo tan valiosas?

Gracias.

+0

me gusta mucho patrón de FI. Acabo de publicar dos blogs en [lectura de datos para objetos de dominio] (http://davidchuprogramming.blogspot.com/2009/04/structuremap-and-fluent-interface.html). –

+0

Una vez escribí una clase de colección que usaba indexadores para agregar a la colección que devolvía la instancia (de colección). El código parecía: 'myCollection [1] [2] [3] [4]' que es similar a 'new [] {1, 2, 3, 4}', por diversión: P – nawfal

Respuesta

8

Felicitaciones por la validación de parámetros de método, me ha dado una nueva idea para nuestras API fluidas. Odié nuestros controles previos a la condición de todos modos ...

Creé un sistema de extensibilidad para un nuevo producto en desarrollo, donde puedes 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 se puede configurar todos los comandos disponibles como esto:

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 opinión configurar sus controles para el menú de edición estándar de comandos utilizando un servicio dado a ellos por el espacio de trabajo, en el que solo lo dicen para observarlos:

Workspace 
    .Observe(control1) 
    .Observe(control2) 

Si las lengüetas de usuario a los controles, el espacio de trabajo para crear automáticamente un adaptador apropiado para el control y proporciona deshacer/rehacer y el portapapeles operaciones.

Nos ha ayudado a reducir drásticamente el código de configuración y hacerlo aún más legible.


me olvidó decirle acerca de una biblioteca que estamos utilizando en nuestros presentadores modelo WinForms MVP para validar los puntos de vista: FluentValidation. ¡Realmente fácil, realmente comprobable, realmente agradable!

+0

Eso es algo excelente. Has creado un DSL para menús y comandos ... y uno bueno por lo que parece. ¿Qué hace es hacer? Tal como Is ()? –

+0

Asocia un tipo de comando (una clase que implementa el patrón de comando) con un nombre de comando lógico (una cadena). Cada invocación utiliza una nueva instancia del tipo. De esta manera, las extensiones pueden invocar comandos utilizando nombres sin saber explícitamente quién/dónde los está implementando realmente. – grover

+0

Oh, lo tengo. Bonito. –

1

El nuevo HttpClient de WCF REST Starter Kit Preview 2 es una gran API fluida. ver mi publicación en el blog para una muestra http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/

+0

¡Genial! Lo estoy viendo ahora mismo. Gracias. –

+0

bendewey ... se preguntaba su opinión sobre esto: ¿podrían haberlo hecho con fluidez, o cree que había una razón particular en este caso para que fuera fluido? –

+0

Si miras el código, es bastante simple. La fluidez proviene principalmente de los métodos de extensión, de los cuales he sido escéptico, pero cuando los utilizo para mejorar esta fluidez, estoy de acuerdo. – bendewey

9

Esta es realmente la primera vez que escucho el término "interfaz fluida". Pero los dos ejemplos que me 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 el encadenamiento de métodos muy potentes

var query = someCollection.Where(x => !x.IsBad).Select(x => x.Property1); 

Los 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. Por lo tanto, crear 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); 
+0

+1 totalmente ignorado linq eso es enorme. Acabo de escuchar el término interfaz fluido el día anterior a esta pregunta. – bendewey

+0

Un problema con esa sintaxis de colección inmutable, especialmente en un bucle, es que funciona igual que concatenando cadenas - tiempo de ejecución exponencial - mejor para llenar una colección normal y luego envolver/copiar en lo inmutable solo una vez. El concepto es bueno sin embargo. – devstuff

+0

@devstuff, usted está haciendo una suposición sobre la naturaleza subyacente de la colección. No todas las colecciones inmutables requieren una operación de copia para agregar elementos (listas vinculadas, árboles de dedo, etc.). Claro, usar un ImmutableArray tendría este problema, pero no es una falla de todos los tipos – JaredPar

1

La API Criteria en NHibernate tiene una interfaz fluida agradable que le permite hacer cosas interesantes 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>(); 
+0

¿Qué va a hacer con eso? Convertirlo en sql? –

+0

Sí, y luego devolver un objeto Entity – lomaxx

2

SubSonic 2.1 tiene una decente para la API de consulta:

DB.Select() 
    .From<User>() 
    .Where(User.UserIdColumn).IsEqualTo(1) 
    .ExecuteSingle<User>(); 

tweetsharp hace un uso extensivo de una API fluida también:

var twitter = FluentTwitter.CreateRequest() 
       .Configuration.CacheUntil(2.Minutes().FromNow()) 
       .Statuses().OnPublicTimeline().AsJson(); 

Y Fluent 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 les utiliza también, pero no pude encontrar un ejemplo rápido.

0

Como se menciona en @John Sheehan, Ninject utiliza este tipo de API para especificar enlaces.Aquí hay un código de ejemplo de su user guide:

Bind<IWeapon>().To<Sword>(); 
Bind<Samurai>().ToSelf(); 
Bind<Shogun>().ToSelf().Using<SingletonBehavior>(); 
7

Me encanta la interfaz fluida en CuttingEdge.Conditions.

Desde 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 

He descubierto que es mucho más fácil de leer, y me hace mucho más eficaz en el control de mis condiciones previas y condiciones (post) en los métodos que cuando tener 50 if declaraciones para manejar los mismos controles.

+0

Cool. No había encontrado esa biblioteca todavía. Lo comprobaré. –

+0

Es muy bueno. Me he sentido feliz de usarlo hasta ahora. –

+0

Personalmente no me gusta Requiere ("id") ... por qué no usar EnsureThat() o algo similar sin cadenas feas. Oh, lo sé, probablemente para hacer excepciones más explicativas. pero aún. –

3

Además de los especificados aquí, el RhinoMocks prueba de unidad popuplar marco simulacro utiliza la sintaxis fluida para especificar las expectativas sobre los objetos de imitación:

// 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); 
2

método de nomenclatura

interfaces fluidas se prestan a la legibilidad siempre que los nombres de los métodos se elijan con sensatez.

Con esto en mente, me gustaría nominar a esta API en particular como "anti-fluidez":

System.Type.IsInstanceOfType

Es un miembro del System.Type y toma un objeto y devuelve true si el objeto es una instancia del tipo.Por desgracia, que, naturalmente, tienden a leer de izquierda a derecha como esto:

o.IsInstanceOfType(t); // wrong 

Cuando en realidad es a la inversa:

t.IsInstanceOfType(o); // right, but counter-intuitive 

Pero no todos los métodos posiblemente 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 estoy señalando otro aspecto de las interfaces fluidas: la elección de los nombres de los métodos para causar la menor sorpresa.

inicializadores de objeto

Con muchos de los ejemplos dados aquí, la única razón se utiliza una interfaz fluida es para que varias propiedades de un objeto recién asignada se pueden inicializar dentro de una sola expresión.

Pero C# tiene una característica del lenguaje que muy a menudo que ello resulte innecesario - objeto sintaxis de inicializador:

var myObj = new MyClass 
      { 
       SomeProperty = 5, 
       Another = true, 
       Complain = str => MessageBox.Show(str), 
      }; 

Esto tal vez explicaría por qué experto en C# usuarios están menos familiarizados con el término "interfaz fluida" para encadenar las llamadas de el mismo objeto - no se necesita tan a menudo en C#.

Como las propiedades pueden tener adaptadores codificados a mano, esta es una oportunidad para invocar 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 colocador propiedad sólo puede aceptar un argumento
  • Un colocador propiedad no puede ser genérico

me gustaría que pudiéramos llamar a los métodos y dar de alta en los 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 solo debería aplicarse inmediatamente después de la construcción? Podríamos tener:

myObj with 
{ 
    SomeProperty = 5, 
    Another = true, 
    Complain = str => MessageBox.Show(str), 
    DoSomething(), 
    Click += (se, ev) => MessageBox.Show("Clicked!"), 
} 

El with habría una nueva palabra clave que opera en un objeto de algún tipo y produce el mismo objeto y tipo - en cuenta que esto sería una expresión, no una declaración. Entonces capturaría exactamente la idea de encadenar en una "interfaz fluida".

De modo que podría usar sintaxis de estilo de inicializador independientemente de si obtuvo el objeto de una expresión new o de un método IOC o de fábrica, etc.

De hecho, usted podría utilizar with después de una completa new y sería equivalente a la actual estilo de inicializador de objeto:

var myObj = new MyClass() with 
      { 
       SomeProperty = 5, 
       Another = true, 
       Complain = str => MessageBox.Show(str), 
       DoSomething(), 
       Click += (se, ev) => MessageBox.Show("Clicked!"), 
      }; 

Y como señala Charlie en los comentarios:

public static T With(this T with, Action<T> action) 
{ 
    if (with != null) 
     action(with); 
    return with; 
} 

El envoltorio anterior simplemente fuerza una acción no regresiva para devolver algo, y listo: cualquier cosa puede ser "fluida" en ese sentido.

Equivalente de inicializador, pero con el evento de reclutamiento:

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 resistir dándole el "tal vez mónada" - check de estilo para nulo también, por lo que si tiene algo que podría devolver null, puede aplicarlo With y luego verificarlo para null -ness.

+0

Puedes hacer algo como esto. Haga un método de extensión en Objeto llamado "Con". Déjelo tomar un Func , y pase el objeto al Func. Ahora, cualquiera sea la lambda que pases, se ejecutará dentro del contexto del objeto. Muy bueno realmente. –

+0

Eso es lógicamente exactamente lo que quiero (aunque debería ser una Acción, no un Func). De acuerdo, entonces la sintaxis es más complicada, pero tomaré algo que pueda usar sobre una extensión de lenguaje imaginaria/improbable. :) –

+0

Sí, el cheque nulo dentro Con es un buen toque. Vamos a seguir siendo fluido incluso si hay un nulo. –

4

Aquí hay una que hice ayer. Un pensamiento más profundo puede llevarme a cambiar el enfoque, pero incluso si es así, el enfoque "fluido" me permite lograr algo que de otro modo no podría tener.

Primero, algunos antecedentes. Recientemente descubrí (aquí en StackOverflow) una forma de pasar un valor a un método para que el método pueda determinar tanto el como el nombre y 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); 
} 

Nota no hay cadena que contiene "lastMonthsInvoice", lo cual es bueno porque chupan cadenas de refactorización. Sin embargo, el mensaje de error puede decir algo así como "El parámetro 'lastMonthsInvoice' no debe ser nulo." Here's the post que explica por qué esto funciona y apunta a la publicación del blog del tipo.

Pero eso solo es 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 a la propiedad como una cadena y luego referirse a ella. Así lo hice en el que podía escribir esto:

ConsoleHelper.WriteProperty(()=> lastMonthsInvoice); 

Y conseguir este resultado:

Property [lastMonthsInvoice] is: <whatever ToString from Invoice 

produce>

Ahora, aquí es donde un enfoque fluidez me permitió hacer algo que de otro modo no podría hacer.

quería hacer ConsoleHelper.WriteProperty tomar una matriz params, por lo que podría volcar muchos tales valores de las propiedades de la consola. Para hacer eso, su firma se vería así:

public static void WriteProperty<T>(params Expression<Func<T>>[] expr) 

por lo que podría hacer esto:

ConsoleHelper.WriteProperty(()=> lastMonthsInvoice,()=> firstName,()=> lastName); 

Sin embargo, que 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 el mismo en todos ellos.

Aquí es donde el enfoque fluido vino al rescate. Hice que WriteProperty() devuelva algo. El tipo que devolvió es algo que puedo llamar And() en. Esto me da la siguiente sintaxis:

ConsoleHelper.WriteProperty(()=> lastMonthsInvoice) 
    .And(()=> firstName) 
    .And(()=> lastName); 

Este es un caso en que el enfoque de fluidez permitió algo que de otro modo no habría sido posible (o al menos no es conveniente).

Aquí está la implementación completa. Como dije, lo escribí ayer. Probablemente verá un margen de mejora o quizás 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() { } 
} 
+0

¡Guau! Eso es realmente genial ... Siempre pensé que la única manera de hacer esto era el reflejo. De hecho, voy a refactorizar algunas cosas ahora mismo. – Jacob

+0

¿No funcionaría 'void WriteProperty (Expression > expr) 'para el ejemplo imposible? – erikkallen

+0

No, eso solo funciona para el ** un ** parámetro. Quiero mostrar muchas propiedades en la consola, sin volver a escribir "ConsoleHelper.WriteProperty" cada vez. Así que primero probé los params, pero si no son todos del mismo tipo, eso no funciona. Fluido, déjame mantener la sintaxis pequeña mientras descarto muchas propiedades. –

1

escribí un pequeño envoltorio de fluidez para System.Net.Mail que encuentro hace que sea un correo electrónico de código 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); 

http://lukencode.com/2010/04/11/fluent-email-in-net/

Cuestiones relacionadas