¿Cuándo ir con fluidez en C #?

78

En muchos aspectos, me gusta mucho la idea de interfaces fluidas, pero con todas las características modernas de C # (inicializadores, lambdas, parámetros con nombre) me encuentro pensando, "¿vale la pena?" Y "¿Es este el patrón correcto para ¿utilizar?". ¿Podría alguien darme, si no una práctica aceptada, al menos su propia experiencia o matriz de decisión sobre cuándo usar el patrón Fluido?

Conclusión:

Algunas buenas reglas generales de las respuestas hasta ahora:

  • Las interfaces fluidas ayudan enormemente cuando tiene más acciones que los configuradores, ya que las llamadas se benefician más del traspaso de contexto.
  • Las interfaces fluidas deben considerarse como una capa sobre la parte superior de una API, no el único medio de uso.
  • Las características modernas, como lambdas, inicializadores y parámetros con nombre, pueden funcionar de la mano para hacer que una interfaz fluida sea aún más amigable.

Aquí hay un ejemplo de lo que quiero decir con las características modernas que lo hacen sentir menos necesario. Tomemos, por ejemplo, una interfaz fluida (quizás un mal ejemplo) que me permite crear un empleado como:

Employees.CreateNew().WithFirstName("Peter")
                     .WithLastName("Gibbons")
                     .WithManager()
                          .WithFirstName("Bill")
                          .WithLastName("Lumbergh")
                          .WithTitle("Manager")
                          .WithDepartment("Y2K");

Podría escribirse fácilmente con inicializadores como:

Employees.Add(new Employee()
              {
                  FirstName = "Peter",
                  LastName = "Gibbons",
                  Manager = new Employee()
                            {
                                 FirstName = "Bill",
                                 LastName = "Lumbergh",
                                 Title = "Manager",
                                 Department = "Y2K"
                            }
              });

También podría haber utilizado parámetros con nombre en los constructores en este ejemplo.

Andrew Hanlon
fuente
1
Buena pregunta, pero creo que es más una pregunta wiki
Ivo
Su pregunta está etiquetada como "nhibernate fluido". Entonces, ¿está tratando de decidir si crea una interfaz fluida o si usa la configuración fluida nhibernate vs. XML?
Ilya Kogan
1
Votado para migrar a Programadores.SE
Matt Ellen
@Ilya Kogan, creo que en realidad está etiquetada como "interfaz fluida", que es una etiqueta genérica para el patrón de interfaz fluido. Esta pregunta no se refiere a nhibernate, sino como dijiste solo si crear una interfaz fluida. Gracias.
1
Esta publicación me inspiró a pensar en una forma de usar este patrón en C. Mi intento se puede encontrar en el sitio hermano de Code Review .
otto

Respuestas:

28

Escribir una interfaz fluida (he incursionado en ello) requiere más esfuerzo, pero tiene una recompensa porque si lo haces bien, la intención del código de usuario resultante es más obvia. Es esencialmente una forma de lenguaje específico de dominio.

En otras palabras, si su código se lee mucho más de lo que está escrito (¿y qué código no lo es?), Entonces debería considerar crear una interfaz fluida.

Las interfaces fluidas tienen más que ver con el contexto, y son mucho más que simples formas de configurar objetos. Como puede ver en el enlace de arriba, utilicé una API con fluidez para lograr:

  1. Contexto (por lo tanto, cuando normalmente realiza muchas acciones en una secuencia con la misma cosa, puede encadenar las acciones sin tener que declarar su contexto una y otra vez).
  2. Descubrimiento (cuando va a objectA.intellisense le da muchas pistas. En mi caso anterior, plm.Led.le da todas las opciones para controlar el LED incorporado, y plm.Network.le da las cosas que puede hacer con la interfaz de red. plm.Network.X10.Le da el subconjunto de acciones de red para dispositivos X10. No obtendrá esto con inicializadores de constructor (a menos que desee tener que construir un objeto para cada tipo diferente de acción, lo que no es idiomático).
  3. Reflexión (no utilizada en el ejemplo anterior): la capacidad de tomar una expresión aprobada en LINQ y manipularla es una herramienta muy poderosa, particularmente en algunas API auxiliares que construí para pruebas unitarias. Puedo pasar una expresión getter de propiedad, construir un montón de expresiones útiles, compilarlas y ejecutarlas, o incluso usar el getter de propiedad para configurar mi contexto.

Una cosa que normalmente hago es:

test.Property(t => t.SomeProperty)
    .InitializedTo(string.Empty)
    .CantBeNull() // tries to set to null and Asserts ArgumentNullException
    .YaddaYadda();

No veo cómo puedes hacer algo así sin una interfaz fluida.

Edición 2 : También puede realizar mejoras de legibilidad realmente interesantes, como:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();
Scott Whitlock
fuente
Gracias por la respuesta, sin embargo, soy consciente de la razón para usar Fluent, pero estoy buscando una razón más concreta para usarlo en algo como mi nuevo ejemplo anterior.
Andrew Hanlon
Gracias por la respuesta extendida. Creo que ha esbozado dos buenas reglas generales: 1) Use Fluent cuando tenga muchas llamadas que se beneficien del 'traspaso' del contexto. 2) Piense en Fluent cuando tenga más llamadas que setters.
Andrew Hanlon
2
@ach, no veo nada en esta respuesta sobre "más llamadas que setters". ¿Te confunde su afirmación sobre "el código [que] se lee mucho más de lo que se escribe"? No se trata de captadores / establecedores de propiedades, se trata de que los humanos lean el código frente a los humanos que escriben el código, se trata de hacer que el código sea fácil de leer para los humanos , porque generalmente leemos una línea de código dada con mucha más frecuencia de la que la modificamos.
Joe White el
@ Joe White, tal vez debería reformular mi término 'llamado' a 'acción'. Pero la idea sigue en pie. Gracias.
Andrew Hanlon
¡La reflexión para probar es malvada!
Adronius
24

Scott Hanselman habla sobre esto en el Episodio 260 de su podcast Hanselminutes con Jonathan Carter. Explican que una interfaz fluida es más como una interfaz de usuario en una API. No debe proporcionar una interfaz fluida como único punto de acceso, sino proporcionarla como una especie de interfaz de usuario de código encima de la "interfaz API normal".

Jonathan Carter también habla un poco sobre el diseño de API en su blog .

Kristof Claes
fuente
Muchas gracias por los enlaces de información y la interfaz de usuario en la parte superior de la API es una buena manera de verlo.
Andrew Hanlon
14

Las interfaces fluidas son características muy poderosas para proporcionar dentro del contexto de su código, cuando se pone en práctica con el razonamiento "correcto".

Si su objetivo es simplemente crear cadenas de código de una línea masivas como una especie de pseudo-caja negra, entonces probablemente esté ladrando el árbol equivocado. Si, por otro lado, lo está utilizando para agregar valor a su interfaz API al proporcionar un medio para encadenar llamadas de método y mejorar la legibilidad del código, entonces con mucha buena planificación y esfuerzo, creo que el esfuerzo vale la pena.

Evitaría seguir lo que parece convertirse en un "patrón" común al crear interfaces fluidas, donde nombras todos tus métodos fluidos "con" -algo, ya que roba una interfaz API potencialmente buena de su contexto y, por lo tanto, de su valor intrínseco. .

La clave es pensar en la sintaxis fluida como una implementación específica de un lenguaje específico de dominio. Como un muy buen ejemplo de lo que estoy hablando, eche un vistazo a StoryQ, que emplea la fluidez como un medio para expresar un DSL de una manera muy valiosa y flexible.

S.Robins
fuente
Gracias por la respuesta, nunca es demasiado tarde para dar una respuesta bien pensada.
Andrew Hanlon
No me importa el prefijo 'con' para los métodos. Los distingue de otros métodos que no devuelven un objeto para encadenar. Ej. position.withX(5)Versusposition.getDistanceToOrigin()
LegendLength
5

Nota inicial: estoy en desacuerdo con una suposición en la pregunta, y extraigo mis conclusiones específicas (al final de esta publicación). Debido a que esto probablemente no sea una respuesta completa que abarque, estoy marcando esto como CW.

Employees.CreateNew().WithFirstName("Peter")…

Podría escribirse fácilmente con inicializadores como:

Employees.Add(new Employee() { FirstName = "Peter",  });

En mi opinión, estas dos versiones deberían significar y hacer cosas diferentes.

  • A diferencia de la versión no fluida, la versión fluida oculta el hecho de que lo nuevo Employeetambién se Addedita en la colección Employees; solo sugiere que un nuevo objeto es Created.

  • El significado de ….WithX(…)es ambiguo, especialmente para las personas que provienen de F #, que tiene una withpalabra clave para las expresiones de objeto : podrían interpretar obj.WithX(x)como un nuevo objeto derivado del objque es idéntico a objexcepción de su Xpropiedad, cuyo valor es x. Por otro lado, con la segunda versión, está claro que no se crean objetos derivados y que todas las propiedades se especifican para el objeto original.

….WithManager().With
  • Esto ….With…tiene otro significado: cambiar el "foco" de la inicialización de la propiedad a un objeto diferente. El hecho de que su API fluida tenga dos significados diferentes Withhace que sea difícil interpretar correctamente lo que está sucediendo ... y tal vez es por eso que usó la sangría en su ejemplo para demostrar el significado deseado de ese código. Sería más claro así:

    (employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
    //                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     value of the `Manager` property appears inside the parentheses,
    //                     like with `WithName`, where the `Name` is also inside the parentheses 
    

Conclusiones: "Ocultar" una función de lenguaje suficientemente simple new T { X = x }, con una API fluida ( Ts.CreateNew().WithX(x)) obviamente se puede hacer, pero:

  1. Se debe tener cuidado de que los lectores del código fluido resultante todavía entiendan exactamente lo que hace. Es decir, la API fluida debe ser transparente en significado y sin ambigüedades. El diseño de una API de este tipo puede ser más difícil de lo previsto (es posible que deba probarse para facilitar su uso y aceptación), y / o ...

  2. diseñarlo podría ser más trabajo del necesario: en este ejemplo, la API fluida agrega muy poca "comodidad del usuario" sobre la API subyacente (una función de lenguaje). Se podría decir que una API fluida debería hacer que la característica subyacente de API / lenguaje sea "más fácil de usar"; es decir, debería ahorrarle al programador una cantidad considerable de esfuerzo. Si es solo otra forma de escribir lo mismo, probablemente no valga la pena, porque no hace que la vida del programador sea más fácil, sino que solo hace que el trabajo del diseñador sea más difícil (vea la conclusión # 1 arriba).

  3. Ambos puntos anteriores suponen silenciosamente que la API fluida es una capa sobre una API existente o una función de idioma. Esta suposición puede ser otra buena guía: una API fluida puede ser una forma adicional de hacer algo, no la única. Es decir, podría ser una buena idea ofrecer una API fluida como una opción "opt-in".

stakx
fuente
1
Gracias por tomarse el tiempo de agregar a mi pregunta. Admitiré que mi ejemplo elegido fue mal pensado. En ese momento estaba buscando usar una interfaz fluida para una API de consulta que estaba desarrollando. Yo simplifiqué demasiado. Gracias por señalar las fallas y por los buenos puntos de conclusión.
Andrew Hanlon
2

Me gusta el estilo fluido, expresa la intención muy claramente. Con el ejemplo de inicializador de objetos que tiene después, debe tener definidores de propiedad pública para usar esa sintaxis, no con el estilo fluido. Dicho esto, con tu ejemplo no ganas mucho sobre los setters públicos porque casi has optado por un estilo de método set / get java-esque.

Lo que me lleva al segundo punto, no estoy seguro de si usaría el estilo fluido de la manera que lo ha hecho, con muchos establecedores de propiedades, probablemente usaría la segunda versión para eso, lo encuentro mejor cuando tienen muchos verbos para encadenar, o al menos muchas cosas en lugar de configuraciones.

Ian
fuente
Gracias por su respuesta, creo que ha expresado una buena regla general: hablar con fluidez es mejor con muchas llamadas en muchos setters.
Andrew Hanlon
1

No estaba familiarizado con el término interfaz fluida , pero me recuerda un par de API que he usado, incluido LINQ .

Personalmente, no veo cómo las características modernas de C # evitarían la utilidad de este enfoque. Prefiero decir que van de la mano. Por ejemplo, es aún más fácil lograr dicha interfaz mediante el uso de métodos de extensión .

Quizás aclare su respuesta con un ejemplo concreto de cómo se puede reemplazar una interfaz fluida utilizando una de las características modernas que mencionó.

Steven Jeuris
fuente
1
Gracias por la respuesta. He agregado un ejemplo básico para ayudar a aclarar mi pregunta.
Andrew Hanlon