Interfaces fluidas
Introducción
Martin Fowler definió la técnica Interfaz fluida (Fluent Interface) como un estilo para construir interfaces (APIs) orientadas a objetos que nos permite escribir un código fácilmente leíble.A continuación, mostraré como se implementa esta técnica con el encadenamiento de métodos (Method Chaining). Después ampliaré el post para ver esta misma técnica con métodos que aceptan como parámetros a delegados (
Actions
o Functions
), dando una mayor flexibilidad y expresividad a esta técnica. Son muchos los frameworks que implementan esta técnica y muchos de ellos empleando delegados. Muchos de ellos los utilizamos a diario como consumidores y poco a poco nos hemos acostumbrado a ellos. Ahora veremos como implementarlos nosotros mismos para nuestras propias interfaces.
Suelen emplearse para realizar acciones secuenciales sobre un mismo objeto. Tal es el caso de las parametrizaciones y transformaciones de los objetos complejos.
Encadenamiento de métodos
Esta técnica consiste en que cada método de un objeto devuelve el propio objeto o al menos un objeto del mismo tipo, permitiendo encadenar las llamadas juntas en una misma sentencia evitando la necesidad de ir guardando los resultados en variables intermedias.En el ejemplo siguiente, los métodos de la clase
Persona
, además de realizar su trabajo específico, devuelven el propio objeto para que el consumidor pueda encadenar las llamadas a sus métodos: Y el uso sería: Aunque puede haber un método finalizador que desencadene una última acción sobre el objeto final no es obligatorio. En caso de existir este método podría no devolver nada (void). El patrón Builder permite encadenar varios métodos de parametrización sobre un objeto builder y terminar con una llamada a un método
build
que nos devuelve una instancia con las características especificadas. El siguiente ejemplo muestra como .Net Core 2.0 configura el objeto IWebHost
haciendo uso de un builder: El método CreateDefaultBuilder
devuelve un objeto IWebHostBuilder
con el que podemos encadenar la configuración del Host con las sucesivas llamadas a métodos en cadena. El último método build
se encarga de crear una instancia de un objeto IWebHost
con la configuración especificada. En el siguiente ejemplo podemos ver la interfaz fluida de la clase
StringBuilder
de la librería System.Text. Construimos una cadena de texto encadenando métodos Append
y AppendLine
gracias a que estos métodos devuelven el propio objeto StringBuilder
: Como podemos observar esta técnica permite escribir una frase uniendo invocaciones a métodos en una misma sentencia. Si a la hora de diseñar la interfaz utilizamos nombres descriptivos de los métodos estaremos dando valor semántico a nuestro código, y por lo tanto escribiremos código compresible y auto-descriptivo. Imaginemos una interfaz que nos permita escribir un código como este:
Incluso sin ser programador se intuye el significado del código anterior. Esto es a lo que Martin Fowler se refiere con interfaz leíble.
En el ejemplo, el método
FabricarCoche
devuelve un objeto Coche
. Este objeto implementa una interfaz fluida con la que podemos configurar nuestro coche a nuestro gusto. Hay que destacar que ni hay un orden obligatorio de invocación ni es necesario llamar a todos y cada uno de los métodos. El consumidor elige qué métodos quiere invocar y en qué orden lo hace.
Que no exista un orden forzoso es intrínseco a la técnica, ya que tendremos acceso a todos los métodos de la interfaz visibles del objeto, estemos en el punto que estemos de la cadena. Si nuestro diseño necesita de una secuencia concreta de invocación deberíamos estudiar otra técnica más adecuada en la que el consumidor no tenga la libertad que ofrece la interfaz fluida.
Eso sí, que sea libre el orden no significa que los resultados deban ser los mismos. Esto depende de la implementación de cada caso particular. Tenemos un ejemplo muy claro con el objeto
StringBuilder
. Las dos líneas siguientes no son equivalentes: Mutando tipos genéricos
Se puede usar la interfaz fluida con tipos genéricos igual que con tipos normales. Incluso se pueda dar el caso que tengamos métodos encadenables que muten el tipo genérico. Para este caso, el objeto sobre el que invocamos el método no será el mismo que devuelva el método. Se devolverá un nuevo objeto del mismo tipo pero con un distinto tipo genérico. Esto es debido a que un tipo genérico no puede cambiar el tipo de su genérico en tiempo de ejecución. Más adelante se verá con más detalle como trabajan los métodos de extensión de la librería Linq del Framework .Net sobre colecciones y como algunos de sus métodos encadenables realizan estas transformaciones sobre los elementos de las colecciones.Antes mostraré un ejemplo muy sencillo donde una interfaz fluida sobre un objeto genérico puede realizar una mutación a un nuevo objeto con un genérico distinto.
En el ejemplo los métodos
Pintar
, ConGps
, Mutar
y MutarABarco
forman la interfaz fluida de la clase Garaje
. Mientras los métodos Pintar
y ConGps
actúan sobre el propio objeto, el método Mutar
crea un nuevo objeto con un tipo genérico resuelto en la llamada y el método MutarABarco
crea un nuevo objeto con el tipo genérico Barco
. El consumidor puede emplear la interfaz de la siguiente manera: Resultado:
En el ejemplo se crea un objeto
Garaje
con tipo genérico Coche
. A continuación utiliza la interfaz fluida llamando a los métodos: Pintar
yConGps
: ambos actúan sobre el objeto con genéricoCoche
que hemos creado y devuelven el mismo objeto.Mutar
: A partir del objeto con genéricoCoche
crea un nuevo objeto con genéricoMoto
y lo devuelve.MutarABarco
: A partir del objeto con genéricoMoto
crea un nuevo objeto con genéricoBarco
y lo devuelve.
Ley de Demeter
Aunque en el post anterior de Composición o herencia: Ser o tener comenté que el encadenamiento de métodos viola la Ley de Demeter, en el caso de interfaces fluidas siempre devolvemos el mismo tipo de objeto, por lo que no estamos saliendo nunca de la relación entre el consumidor y el objeto respetando así esta ley. Recordemos que la Ley de Demeter se viola cuando la cadena de métodos va trabajando sobre objetos distintos en cada invocación, ya que en cada método introducimos una nueva dependencia.
Interfaz fluida compleja o anidada
Las interfaces fluidas nos permiten trabajar con objetos compuestos de otros objetos que a su vez utilizan también sus propias interfaces fluidas. Esto permite encadenar métodos a distintos niveles. En el siguiente ejemplo hemos diseñado una interfaz fluida compleja:Esta vez, el método
ConMotor
acepta un objeto Motor
como parámetro. En lugar de pasarle un objeto Motor
llamamos al método FabricarMotor
, el cual nos devuelve un nuevo objeto Motor
. Este objeto implementa su propia interfaz fluida para configurar el motor del coche. En la siguiente sección daremos un paso más en fluidez utilizando expresiones lambda como parámetros. También veremos en detalle la interfaz fluida que utiliza Linq con objetos IEnumerables.
Usando Lambdas
Al usar delegados en los parámetros de nuestros métodos estamos delegando en el consumidor parte de la responsabilidad de su comportamiento. Si estos delegados además son sencillos se pueden implementar mediante expresiones lambda dando a las sentencias de métodos encadenados mayor expresividad.A continuación mostraré un ejemplo de una interfaz fluida anidada utilizando expresiones lambda en algunos de sus métodos. A partir de la clase
Garaje
iremos profundizando paso por paso hasta completar la interfaz: Los métodos que forman parte de la interfaz fluida se identifican rápidamente porque todos ellos retornan el propio objeto (this
). Veamos uno a uno: - El método
MaximaCapacidad
simplemente establece un número entero. - El método
ConfiguraCoche
acepta un delegado de tipoAction
con dos parámetros de entrada. Esto permite al consumidor pasarnos un delegado en el que podrá hacer uso de un parámetro de tipoCoche
y otro de tipoPerfil
. Cuando este método sea llamado, invocaremos el Action con los parámetros que necesita. - El método
ConfiguraMoto
yConfiguraBici
también aceptan un delegado pero esta vez con un sólo parámetro de entrada. Cada uno de ellos acepta el propio objeto que se pretende configurar.
garaje
instanciado permite encadenar los métodos de la interfaz. Los métodos que aceptan delegados abren un nuevo nivel en la interfaz donde el contexto es trasladado a cada elemento que estamos configurando. Por ejemplo, entre los corchetes { } de la expresión lambda del método ConfiguraCoche
podremos empezar a parametrizar el objeto coche
. En este caso nos proporciona también el objeto Perfil
que podremos utilizar (o no) según nos convenga. Para continuar veamos ahora la interfaz del objeto
Coche
: Si analizamos los métodos que aceptan delegados tenemos: - El método
ConfiguraMotor
acepta unAction
con un parámetro de entrada del propio objetoMotor
que estamos configurando. En este caso se instancia un objetoMotorCoche
que es una extensión del objetoMotor
. - El método
Ruedas
también acepta unAction
con un parámetro de tipoRuedas
. En este caso se trata de un listado de elementos de tipoRueda
. - El método
FuncionConsumo
acepta un delegado de tipoFunc
. En este caso en vez de invocarlo inmediatamente lo almacena para usarlo como función de cálculo cada vez que se invoque el métodoCalculaConsumo
. El delegado se convierte así en parte de la parametrización del objeto, solo que en lugar de almacenar un valor lo que hace es almacenar un comportamiento.
Esta técnica permite acceder desde un contexto de nivel inferior a las variables de contextos de niveles superiores. Así por ejemplo, si necesitamos hacer uso de la variable
perfil
proporcionada en el contexto de ConfigurarCoche
desde dentro del contexto de ConfigurarMotor
la podemos utilizar sin problemas. En todos los métodos con delegados del ejemplo anterior se han invocado los delegados recibidos en el mismo momento en que se han recibido. Podría darse el caso que nos interesase ir almacenando todos los delegados en la clase e invocar todos ellos en un lote mediante una única llamada de inicialización al terminar de parametrizar los objetos.
Veamos algunos ejemplos de frameworks que utilizan interfaces fluidas con expresiones lambdas.
Swagger
Como ejemplo de framework que utiliza una interfaz fluida tenemos Swagger . Este framework permite documentar una Web API de manera muy eficiente. Para configurar esta herramienta dispone de una interfaz fluida con una técnica similar a la que hemos utilizado en el ejemplo anterior. Se trata de una interfaz diseñada para trabajar con expresiones lambda y que además dispone de varios niveles de complejidad:En el ejemplo se puede apreciar los distintos niveles de complejidad de la interfaz y los distintos métodos que expone cada nivel. Algunos de ellos son métodos con parámetros delegados. Cabe destacar que el código mostrado no es mas que una pequeña muestra de la cantidad de métodos y niveles que dispone esta interfaz para su configuración.
Linq
Los métodos de extensión para el objetoIEnumerable
de la librería Linq del Framework de .Net es un claro ejemplo de lo expresiva que resulta la manipulación de elementos utilizando el estilo de la interfaz fluida con expresiones lambda. En el ejemplo se puede interpretar fácilmente el proceso llevado a cabo en cada paso: - Como entrada utiliza el objeto personas que es de tipo
IEnumerable
que representa una colección de objetos de tipoPersona
. Podría tratarse de cualquier colección que implemente esta interfaz. - El método
Where
filtra todas las personas de la colección cuyo apellido empieza por la letra "A". Este método a su vez devuelve otro objetoIEnumerable
también con elementos de tipoPersona
. - El método
OrderBy
ordena este resultado por la edad. Este método también devuelve un objetoIEnumerable
con elementos de tipoPersona
. - Finalmente, el método
Select
, aunque devuelve también una colecciónIEnumerable
, esta vez no es de elementosPersona
, se han proyectado hacia elementosstring
mediante la función proporcionada. El métodoSelect
tiene la capacidad de convertir (proyectar) cada elemento en otro según la función que le pasamos como parámetro. En nuestro caso la función proyecta cada elemento de tipoPersona
en unstring
que representa la concatenación del nombre y el apellido.
IEnumerable
y devuelve un nuevo objeto IEnumerable
con los elementos del resultado, que no tienen por qué ser del mismo tipo que los elementos de origen. La proyección no está produciendo un cambio del tipo, que en este caso siempre es IEnumerable, sino que cambia el tipo de sus elementos.En el ejemplo siguiente, como no hay proyección, obtendremos un resultado idéntico a la colección inicial. Sin embargo, se ve claramente que no es lo mismo filtrar todas las personas cuyo apellido empiece por "A" y quedarte con los 10 primeros, que quedarte con los 10 primeros y filtrar de esos 10 sólo los que empiecen por "A":
Arboles de expresión y reflexión estática
Queda en el tintero el diseño de interfaces fluidas con árboles de expresión. Este tipo de parámetros nos permite todavía más flexibilidad que la de un simple delegado. Además de la potencia de los delegados nos ofrecen la capacidad de disponer de la expresión sin compilar. De esta forma tenemos la expresión almacenada como una estructura de árbol en la que los nodos representan los operadores y las hojas a los operandos.Se trata de un tema demasiado extenso para exponerlo dentro de las interfaces fluidas y he decidido publicarlo en dos futuros posts donde se explicarán más detalladamente el uso de estos árboles y la reflexión estática donde las interfaces fluidas pueden aprovechar ciertas características extra.
Como adelanto dejo aquí un enlace a los frameworks Rhino Mocks y Moq que hacen uso de la reflexión estática con expresiones de árbol para ofrecer una interfaz fluida al consumidor. Se trata de frameworks opensource para .Net que ayudan a crear implementaciones mock para realizar tests.
Written on November 15, 2017