En ingeniería de software , una interfaz fluida es una API orientada a objetos cuyo diseño se basa en gran medida en el encadenamiento de métodos . Su objetivo es aumentar la legibilidad del código mediante la creación de un lenguaje específico de dominio (DSL). El término fue acuñado en 2005 por Eric Evans y Martin Fowler . [1]
Implementación
Una interfaz fluida se implementa normalmente mediante el uso del encadenamiento de métodos para implementar el método en cascada (en lenguajes que no lo admiten de forma nativa), concretamente haciendo que cada método devuelva el objeto al que está adjunto, a menudo denominado this
o self
. Dicho de manera más abstracta, una interfaz fluida transmite el contexto de instrucción de una llamada posterior en el encadenamiento de métodos, donde generalmente el contexto es
- Definido a través del valor de retorno de un método llamado
- Autorreferencial , donde el nuevo contexto es equivalente al último contexto
- Terminado por el regreso de un contexto vacío
Tenga en cuenta que una "interfaz fluida" significa más que un método en cascada a través del encadenamiento; implica diseñar una interfaz que se lea como un DSL, utilizando otras técnicas como "funciones anidadas y alcance de objetos". [1]
Historia
El término "interfaz fluida" se acuñó a finales de 2005, aunque este estilo general de interfaz se remonta a la invención del método en cascada en Smalltalk en la década de 1970, y numerosos ejemplos en la década de 1980. Un ejemplo común es la biblioteca iostream en C ++ , que usa los operadores<<
o para el paso del mensaje, enviando múltiples datos al mismo objeto y permitiendo "manipuladores" para otras llamadas a métodos. Otros ejemplos tempranos incluyen el sistema Garnet (de 1988 en Lisp) y el sistema Amulet (de 1994 en C ++) que usaba este estilo para la creación de objetos y la asignación de propiedades.>>
Ejemplos de
C#
C # usa una programación fluida ampliamente en LINQ para construir consultas usando "operadores de consulta estándar". La implementación se basa en métodos de extensión .
var traducciones = new Dictionary < string , string > { { "gato" , "chat" }, { "perro" , "chien" }, { "pez" , "poisson" }, { "pájaro" , "oiseau" } };// Encuentra traducciones de palabras en inglés que contienen la letra "a", // ordenados por la longitud y se muestra en mayúsculas IEnumerable < string > consulta = traducciones . Donde ( t => t . Clave . Contiene ( "a" )) . OrderBy ( t => t . Valor . Longitud ) . Seleccione ( t => t . Valor . ToUpper ());// La misma consulta construida progresivamente: var filtrada = traducciones . Donde ( t => t . Clave . Contiene ( "a" )); var ordenado = filtrado . OrderBy ( t => t . Valor . Longitud ); var finalQuery = ordenado . Seleccione ( t => t . Valor . ToUpper ());
La interfaz fluida también se puede usar para encadenar un conjunto de métodos, que opera / comparte el mismo objeto. En lugar de crear una clase de cliente, podemos crear un contexto de datos que se puede decorar con una interfaz fluida de la siguiente manera.
// Define la clase de contexto de datos Contexto { cadena pública FirstName { get ; establecer ; } cadena pública Apellido { get ; establecer ; } cadena pública Sex { get ; establecer ; } dirección de cadena pública { get ; establecer ; } } clase Cliente { contexto privado _contexto = nuevo contexto (); // Inicializa el contexto // establecer el valor de las propiedades public Customer FirstName ( string firstName ) { _context . FirstName = firstName ; devuelve esto ; } pública Cliente Apellido ( cadena lastName ) { _context . LastName = lastName ; devuelve esto ; } pública al cliente Sexo ( cadena de sexo ) { _context . Sexo = sexo ; devuelve esto ; } Dirección pública del cliente ( dirección de cadena ) { _context . Dirección = dirección ; devuelve esto ; } // Imprime los datos en la consola public void Print () { Console . WriteLine ( $ "Nombre: {_context.FirstName} \ nÚltimo nombre: {_context.LastName} \ nSex: {_context.Sex} \ nAddress: {_context.Address}" ); } }class Program { static void Main ( string [] args ) { // Creación de objeto Customer c1 = new Customer (); // Usando el método de encadenamiento para asignar e imprimir datos con una sola línea c1 . Nombre ( "vinod" ). Apellido ( "srivastav" ). Sexo ( "masculino" ). Dirección ( "bangalore" ). Imprimir (); } }
C ++
Un uso común de la interfaz fluida en C ++ es el iostream estándar , que encadena operadores sobrecargados .
El siguiente es un ejemplo de cómo proporcionar un contenedor de interfaz fluido sobre una interfaz más tradicional en C ++:
// básico definición de clase GlutApp { privado : int w_ , h_ , x_ , y_ , argc_ , display_mode_ ; char ** argv_ ; char * title_ ; público : GlutApp ( int argc , char ** argv ) { argc_ = argc ; argv_ = argv ; } void setDisplayMode ( modo int ) { display_mode_ = modo ; } int getDisplayMode () { return display_mode_ ; } vacío setWindowSize ( int w , int h ) { w_ = w ; h_ = h ; } void setWindowPosition ( int x , int y ) { x_ = x ; y_ = y ; } void setTitle ( const char * title ) { title_ = title ; } void create () {;} }; // Uso básico int principal ( int argc , char ** argv ) { GlutApp aplicación ( argc , argv ); aplicación . setDisplayMode ( GLUT_DOUBLE | GLUT_RGBA | GLUT_ALPHA | GLUT_DEPTH ); // Establecer la aplicación de parámetros de framebuffer . setWindowSize ( 500 , 500 ); // Establecer la aplicación de parámetros de ventana . setWindowPosition ( 200 , 200 ); aplicación . setTitle ( "Mi aplicación OpenGL / GLUT" ); aplicación . crear (); } // Clase contenedora fluida FluentGlutApp : private GlutApp { public : FluentGlutApp ( int argc , char ** argv ) : GlutApp ( argc , argv ) {} // Heredar el constructor padre FluentGlutApp & withDoubleBuffer () { setDisplayMode ( getDisplayMode () | GLUT_DOUBLE ) ; devolver * esto ; } FluentGlutApp & withRGBA () { setDisplayMode ( getDisplayMode () | GLUT_RGBA ); devolver * esto ; } FluentGlutApp & withAlpha () { setDisplayMode ( getDisplayMode () | GLUT_ALPHA ); devolver * esto ; } FluentGlutApp & withDepth () { setDisplayMode ( getDisplayMode () | GLUT_DEPTH ); devolver * esto ; } FluentGlutApp & across ( int w , int h ) { setWindowSize ( w , h ); devolver * esto ; } FluentGlutApp & at ( int x , int y ) { setWindowPosition ( x , y ); devolver * esto ; } FluentGlutApp & named ( const char * title ) { setTitle ( título ); devolver * esto ; } // No tiene sentido encadenar después de create (), así que no devuelva * este vacío create () { GlutApp :: create (); } }; // Uso fluido int main ( int argc , char ** argv ) { FluentGlutApp ( argc , argv ) . withDoubleBuffer (). conRGBA (). withAlpha (). withDepth () . en ( 200 , 200 ). a través ( 500 , 500 ) . named ( "Mi aplicación OpenGL / GLUT" ) . crear (); }
Java
La biblioteca jOOQ modela SQL como una API fluida en Java. Un ejemplo de una expectativa de prueba fluida en el marco de prueba jMock es: [1]
burlarse . espera ( una vez ()). método ( "m" ). with ( o ( stringContains ( "hola" ), stringContains ( "hola" )) );
Autor autor = AUTOR . como ( "autor" ); crear . selectFrom ( autor ) . donde ( existe ( selectOne () . from ( BOOK ) . where ( BOOK . STATUS . eq ( BOOK_STATUS . SOLD_OUT )) . y ( BOOK . AUTHOR_ID . eq ( ID . autor ))));
El procesador de anotaciones fluflu permite la creación de una API fluida utilizando anotaciones Java.
La biblioteca JaQue permite representar Java 8 Lambdas como objetos en forma de árboles de expresión en tiempo de ejecución, lo que permite crear interfaces fluidas con seguridad de tipos, es decir, en lugar de:
Cliente obj = ... obj . propiedad ( "nombre" ). eq ( "Juan" )
Se puede escribir:
método < Cliente > ( cliente -> cliente . getName () == "John" )
Además, la biblioteca de pruebas de objetos simulados EasyMock hace un uso extensivo de este estilo de interfaz para proporcionar una interfaz de programación expresiva.
Colección mockCollection = EasyMock . createMock ( Colección . clase ); EasyMock . esperar ( mockCollection . eliminar ( nulo )) . andThrow ( nueva NullPointerException ()) . atLeastOnce ();
En la API de Java Swing, la interfaz LayoutManager define cómo los objetos Container pueden controlar la ubicación de los componentes. Una de las LayoutManager
implementaciones más poderosas es la clase GridBagLayout que requiere el uso de la GridBagConstraints
clase para especificar cómo ocurre el control de diseño. Un ejemplo típico del uso de esta clase es algo como el siguiente.
GridBagLayout gl = new GridBagLayout (); JPanel p = nuevo JPanel (); p . setLayout ( gl );JLabel l = new JLabel ( "Nombre:" ); JTextField nm = nuevo JTextField ( 10 );GridBagConstraints gc = new GridBagConstraints (); gc . cuadrículax = 0 ; gc . cuadriculado = 0 ; gc . fill = GridBagConstraints . NINGUNO ; p . sumar ( l , gc );gc . cuadrículax = 1 ; gc . fill = GridBagConstraints . HORIZONTAL ; gc . pesox = 1 ; p . añadir ( nm , gc );
Esto crea una gran cantidad de código y dificulta ver qué está sucediendo exactamente aquí. La Packer
clase proporciona un mecanismo fluido, por lo que debería escribir: [2]
JPanel p = nuevo JPanel (); Packer pk = nuevo Packer ( p );JLabel l = new JLabel ( "Nombre:" ); JTextField nm = nuevo JTextField ( 10 );pk . paquete ( l ). gridx ( 0 ). cuadriculado ( 0 ); pk . paquete ( nm ). gridx ( 1 ). cuadriculado ( 0 ). fillx ();
Hay muchos lugares donde las API fluidas pueden simplificar la forma en que se escribe el software y ayudar a crear un lenguaje API que ayude a los usuarios a ser mucho más productivos y cómodos con la API porque el valor de retorno de un método siempre proporciona un contexto para futuras acciones en ese contexto.
JavaScript
Hay muchos ejemplos de bibliotecas JavaScript que usan alguna variante de esto: jQuery probablemente sea la más conocida. Normalmente, los constructores fluidos se utilizan para implementar "consultas de base de datos", por ejemplo, en https://github.com/Medium/dynamite :
// obteniendo un artículo de un cliente de mesa . getItem ( 'tabla de usuario' ) . setHashKey ( 'userId' , 'userA' ) . setRangeKey ( 'columna' , '@' ) . ejecutar () . luego ( función ( datos ) { // datos.resultado: el objeto resultante })
Una forma sencilla de hacer esto en JavaScript es usando la herencia de prototipos y this
.
// ejemplo de https://schier.co/blog/2013/11/14/method-chaining-in-javascript.htmlclase Gatito { constructor () { esto . nombre = 'Garfield' ; esto . color = 'naranja' ; } setName ( nombre ) { esto . nombre = nombre ; devuelve esto ; } setColor ( color ) { esto . color = color ; devuelve esto ; } save () { consola . log ( `guardando $ { este . nombre } , el $ { este . color } gatito` ); devuelve esto ; } }// utilícelo nuevo Kitten () . setName ( 'Salem' ) . setColor ( 'negro' ) . guardar ();
Scala
Scala admite una sintaxis fluida tanto para llamadas a métodos como para combinaciones de clases , utilizando rasgos y la with
palabra clave. Por ejemplo:
class Color { def rgb () : Tuple3 [ Decimal ] } objeto Black amplía Color { override def rgb () : Tuple3 [ Decimal ] = ( "0" , "0" , "0" ); }trait GUIWindow { // Métodos de renderizado que devuelven esto para un dibujo fluido def set_pen_color ( color : Color ) : this.type def move_to ( pos : Position ) : this.type def line_to ( pos : Position , end_pos : Position ) : this.type def render () : esto. type = this // No dibujes nada, solo devuelve esto, para que las implementaciones secundarias lo usen con fluidez def top_left () : Posición def bottom_left () : Posición def top_right () : Posición def bottom_right () : Posición }El rasgo WindowBorder extiende GUIWindow { def render () : GUIWindow = { super . render () . mover_a ( arriba_izquierda ()) . set_pen_color ( negro ) . línea_a ( arriba_derecha ()) . línea_a ( abajo_derecha ()) . line_to ( bottom_left ()) . line_to ( top_left ()) } }class SwingWindow extiende GUIWindow { ... }val appWin = new SwingWindow () con WindowBorder appWin . render ()
Raku
En Raku , hay muchos enfoques, pero uno de los más simples es declarar atributos como lectura / escritura y usar la given
palabra clave. Las anotaciones de tipo son opcionales, pero la escritura gradual nativa hace que sea mucho más seguro escribir directamente en atributos públicos.
clase Empleado { subconjunto Salario del real donde *> 0 ; subconjunto NonEmptyString de Str donde * ~~ / \ S / ; # al menos un carácter sin espacio tiene NonEmptyString $ .name es rw ; tiene NonEmptyString $ .surname es rw ; tiene Salario $ .salary es rw ; método gist { return qq: to [END]; Nombre: $ .name Apellido: $ .surname Salario: $ .salary END }}my $ employee = Empleado . nuevo ();dado $ empleado { . nombre = 'Sally' ; . apellido = 'Paseo' ; . salario = 200 ;}digamos $ empleado ;# Salida: # Nombre: Sally # Apellido: Viaje # Salario: 200
PHP
En PHP , uno puede devolver el objeto actual usando la $this
variable especial que representa la instancia. Por return $this;
lo tanto, hará que el método devuelva la instancia. El siguiente ejemplo define una clase Employee
y tres métodos para establecer su nombre, apellido y salario. Cada uno devuelve la instancia de la Employee
clase que permite encadenar métodos.
clase Empleado { cadena privada $ nombre ; cadena privada $ surName ; cadena privada $ salario ; función pública setName ( cadena $ nombre ) { $ esto -> nombre = $ nombre ; return $ this ; } función pública setSurname ( cadena $ apellido ) { $ esto -> apellido = $ apellido ; return $ this ; } función pública setSalary ( cadena $ salario ) { $ esto -> salario = $ salario ; return $ this ; } función pública __toString () { $ employeeInfo = 'Nombre:' . $ this -> nombre . PHP_EOL ; $ employeeInfo . = 'Apellido:' . $ esto -> apellido . PHP_EOL ; $ employeeInfo . = 'Salario:' . $ esto -> salario . PHP_EOL ; return $ employeeInfo ; } }# Cree una nueva instancia de la clase Employee, Tom Smith, con un salario de 100: $ employee = ( new Employee ()) -> setName ( 'Tom' ) -> setSurname ( 'Smith' ) -> setSalary ( '100 ' );# Muestra el valor de la instancia de Empleado: echo $ empleado ;# Pantalla: # Nombre: Tom # Apellido: Smith # Salario: 100
Pitón
En Python , regresar self
en el método de instancia es una forma de implementar el patrón fluido.
Sin embargo , el creador del lenguaje , Guido van Rossum, lo desaconseja y, por lo tanto, se lo considera atípico (no idiomático).
clase Poema : def __init__ ( self , title : str ) -> None : self . title = titulo def indent ( self , espacios : int ): "" "Sangra el poema con el número especificado de espacios." "" self . title = "" * espacios + self . título de retorno yo def sufijo ( self , author : str ): "" "Sufijo el poema con el nombre del autor." "" self . title = f " { self . title } - { author } " return self
>>> Poema ( "Camino no transitado" ) . sangría ( 4 ) . sufijo ( "Robert Frost" ) . título 'Road Not Traveled - Robert Frost'
Rápido
En Swift 3.0+, regresar self
a las funciones es una forma de implementar el patrón fluido.
class Persona { var firstname : String = "" var lastname : String = "" var favoriteQuote : String = "" @ DiscardableResult func conjunto ( primer nombre : Cadena ) -> Ser { auto . firstname = firstname return self } @ discardableResult func set ( apellido : String ) -> Self { self . lastname = lastname return self } @ discardableResult func set ( favoriteQuote : String ) -> Self { self . favoriteQuote = favoriteQuote return self } }
let person = Person () . conjunto ( primer nombre : "Juan" ) . set ( apellido : "Doe" ) . set ( favoriteQuote : "Me gustan las tortugas" )
Inmutabilidad
Es posible crear interfaces fluidas inmutables que utilizan semántica de copia en escritura . En esta variación del patrón, en lugar de modificar las propiedades internas y devolver una referencia al mismo objeto, el objeto se clona, con las propiedades cambiadas en el objeto clonado, y ese objeto se devuelve.
El beneficio de este enfoque es que la interfaz se puede utilizar para crear configuraciones de objetos que pueden bifurcarse desde un punto en particular; Permitir que dos o más objetos compartan una cierta cantidad de estado y se utilicen más sin interferir entre sí.
Ejemplo de JavaScript
Usando la semántica de copia en escritura, el ejemplo de JavaScript anterior se convierte en:
clase Gatito { constructor () { esto . nombre = 'Garfield' ; esto . color = 'naranja' ; } setName ( nombre ) { copia constante = nuevo Gatito (); copia . color = esto . el color ; copia . nombre = nombre ; devolver copia ; } setColor(color) { const copy = new Kitten(); copy.name = this.name; copy.color = color; return copy; } // ...}// use itconst kitten1 = new Kitten() .setName('Salem');const kitten2 = kitten1 .setColor('black');console.log(kitten1, kitten2);// -> Kitten({ name: 'Salem', color: 'orange' }), Kitten({ name: 'Salem', color: 'black' })
Problemas
Errors can not be captured at compile time
In typed languages, using a constructor requiring all parameters will fail at compilation time while the fluent approach will only be able to generate runtime errors, missing all the type-safety checks of modern compilers. It also contradicts the "fail-fast" approach for error protection.
Debugging and error reporting
Single-line chained statements may be more difficult to debug as debuggers may not be able to set breakpoints within the chain. Stepping through a single-line statement in a debugger may also be less convenient.
java.nio.ByteBuffer.allocate(10).rewind().limit(100);
Another issue is that it may not be clear which of the method calls caused an exception, in particular if there are multiple calls to the same method. These issues can be overcome by breaking the statement into multiple lines which preserves readability while allowing the user to set breakpoints within the chain and to easily step through the code line by line:
java.nio.ByteBuffer .allocate(10) .rewind() .limit(100);
However, some debuggers always show the first line in the exception backtrace, although the exception has been thrown on any line.
Logging
Adding logging into the middle of a chain of fluent calls can be an issue. E.g., given:
ByteBuffer buffer = ByteBuffer.allocate(10).rewind().limit(100);
To log the state of buffer
after the rewind()
method call, it is necessary to break the fluent calls:
ByteBuffer buffer = ByteBuffer.allocate(10).rewind();log.debug("First byte after rewind is " + buffer.get(0));buffer.limit(100);
This can be worked around in languages that support extension methods by defining a new extension to wrap the desired logging functionality, for example in C# (using the same Java ByteBuffer example as above):
static class ByteBufferExtensions{ public static ByteBuffer Log(this ByteBuffer buffer, Log log, Action<ByteBuffer> getMessage) { string message = getMessage(buffer); log.debug(message); return buffer; } }// Usage:ByteBuffer .Allocate(10) .Rewind() .Log( log, b => "First byte after rewind is " + b.Get(0) ) .Limit(100);
Subclasses
Subclasses in strongly typed languages (C++, Java, C#, etc.) often have to override all methods from their superclass that participate in a fluent interface in order to change their return type. For example:
class A { public A doThis() { ... }}class B extends A{ public B doThis() { super.doThis(); return this; } // Must change return type to B. public B doThat() { ... }}...A a = new B().doThat().doThis(); // This would work even without overriding A.doThis().B b = new B().doThis().doThat(); // This would fail if A.doThis() wasn't overridden.
Languages that are capable of expressing F-bound polymorphism can use it to avoid this difficulty. For example:
abstract class AbstractA<T extends AbstractA<T>> {@SuppressWarnings("unchecked")public T doThis() { ...; return (T)this; }}class A extends AbstractA<A> {}class B extends AbstractA<B> {public B doThat() { ...; return this; }}...B b = new B().doThis().doThat(); // Works!A a = new A().doThis(); // Also works.
Note that in order to be able to create instances of the parent class, we had to split it into two classes — AbstractA
and A
, the latter with no content (it would only contain constructors if those were needed). The approach can easily be extended if we want to have sub-subclasses (etc.) too:
abstract class AbstractB<T extends AbstractB<T>> extends AbstractA<T> {@SuppressWarnings("unchecked")public T doThat() { ...; return (T)this; }}class B extends AbstractB<B> {}abstract class AbstractC<T extends AbstractC<T>> extends AbstractB<T> {@SuppressWarnings("unchecked")public T foo() { ...; return (T)this; }}class C extends AbstractC<C> {}...C c = new C().doThis().doThat().foo(); // Works!B b = new B().doThis().doThat(); // Still works.
In a dependently typed language, e.g. Scala, methods can also be explicitly defined as always returning this
and thus can be defined only once for subclasses to take advantage of the fluent interface:
class A { def doThis(): this.type = { ... } // returns this, and always this.}class B extends A{ // No override needed! def doThat(): this.type = { ... }}...val a: A = new B().doThat().doThis(); // Chaining works in both directions.val b: B = new B().doThis().doThat(); // And, both method chains result in a B!
Ver también
- Command-query separation
- Method chaining
- Named parameter
- Pipeline (Unix)
Referencias
- ^ a b c Martin Fowler, "FluentInterface", 20 December 2005
- ^ "Interface Pack200.Packer". Oracle. Retrieved 13 November 2019.
enlaces externos
- Martin Fowler's original bliki entry coining the term
- A Delphi example of writing XML with a fluent interface
- A .NET fluent validation library written in C#
- A tutorial for creating formal Java fluent APIs from a BNF notation
- Fluent Interfaces are Evil
- Developing a fluent api is so cool