stringtranslate.com

Interfaz fluida

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 del dominio (DSL). El término fue acuñado en 2005 por Eric Evans y Martin Fowler . [1]

Implementación

Una interfaz fluida normalmente se implementa mediante el uso de encadenamiento de métodos para implementar la cascada de métodos (en lenguajes que no admiten la cascada de forma nativa), concretamente haciendo que cada método devuelva el objeto al que está asociado [ cita requerida ] , a menudo denominado thiso self. Dicho de forma 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

Tenga en cuenta que una "interfaz fluida" significa más que simplemente la conexión en cascada de métodos mediante 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 data de la invención de la cascada de métodos en Smalltalk en la década de 1970, y de numerosos ejemplos en la década de 1980. Un ejemplo común es la biblioteca iostream en C++ , que utiliza los operadores<< or para el paso de mensajes, enviando múltiples datos al mismo objeto y permitiendo "manipuladores" para otras llamadas de método. Otros ejemplos tempranos incluyen el sistema Garnet (de 1988 en Lisp) y el sistema Amulet (de 1994 en C++) que utilizaban este estilo para la creación de objetos y la asignación de propiedades.>>

Ejemplos

DO#

C# utiliza programación fluida de forma extensiva en LINQ para crear consultas mediante "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 contengan la letra "a", // ordenadas por longitud y mostradas en mayúsculas IEnumerable < string > query = translates . Where ( t => t . Key . Contains ( "a" )) . OrderBy ( t => t . Value . Length ) . Select ( t => t . Value . ToUpper ());         // La misma consulta construida progresivamente: var filtered = translates . Where ( t => t . Key . Contains ( "a" )); var sorted = filtered . OrderBy ( t => t . Value . Length ); var finalQuery = sorted . Select ( t => t . Value . ToUpper ());                 

La interfaz fluida también se puede utilizar para encadenar un conjunto de métodos que operan o comparten el mismo objeto. En lugar de crear una clase de cliente, podemos crear un contexto de datos que se puede decorar con la interfaz fluida de la siguiente manera.

// Define la clase de contexto de datos Contexto { cadena pública Nombre { obtener ; establecer ; } cadena pública Apellido { obtener ; establecer ; } cadena pública Sexo { obtener ; establecer ; } cadena pública Dirección { obtener ; establecer ; } }                             clase Cliente { contexto privado _contexto = nuevo contexto (); // Inicializa el contexto         // establece el valor de las propiedades public Customer FirstName ( string firstName ) { _context.FirstName = firstName ; return this ; }            público Apellido del cliente ( cadena apellido ) { _context . Apellido = apellido ; devolver esto ; }           público Cliente Sexo ( cadena sexo ) { _context . Sexo = sexo ; devuelve esto ; }           Dirección del cliente pública ( cadena dirección ) { _context.Dirección = dirección ; devolver esto ; }           // Imprime los datos en la consola public void Print () { Console . WriteLine ( $"Nombre: {_context.FirstName} \nApellido: {_context.LastName} \nSexo: {_context.Sex} \nDirección: {_context.Address}" ); } }      clase Programa { static void Main ( string [] args ) { // Creación de objeto Cliente c1 = new Cliente (); // Uso del método encadenamiento para asignar e imprimir datos con una sola línea c1 . Nombre ( "vinod" ). Apellido ( "srivastav" ). Sexo ( "masculino" ). Dirección ( "bangalore" ). Imprimir (); } }               

El marco de pruebas .NET NUnit utiliza una combinación de métodos y propiedades de C# en un estilo fluido para construir sus afirmaciones "basadas en restricciones" :

Afirmar . Que (() => 2 * 2 , es . Al Menos ( 3 ). Y. Al Máximo ( 5 ));     

C++

Un uso común de la interfaz fluida en C++ es el estándar iostream , que encadena operadores sobrecargados .

El siguiente es un ejemplo de cómo proporcionar una interfaz fluida sobre una interfaz más tradicional en C++:

 // Definición básica clase GlutApp { privado : int w_ , h_ , x_ , y_ , argc_ , modo_de_visualización ; char ** argv_ ; char * título_ ; público : GlutApp ( int argc , char ** argv ) { argc_ = argc ; argv_ = argv ; } void setDisplayMode ( int modo ) { modo_de_visualización_ = modo ; } int getDisplayMode () { return modo_de_visualización ; } void setWindowSize ( int w , int h ) { w_ = w ; h_ = h ; } void setWindowPosition ( int x , int y ) { x_ = x ; y_ = y ; } void setTitle ( const char * título ) { título_ = título ; } void create (){;} }; // Uso básico int main ( int argc , char ** argv ) { GlutApp app ( argc , argv ); app . setDisplayMode ( GLUT_DOUBLE | GLUT_RGBA | GLUT_ALPHA | GLUT_DEPTH ); // Establecer parámetros de framebuffer app . setWindowSize ( 500 , 500 ); // Establecer parámetros de ventana app . setWindowPosition ( 200 , 200 ); app . setTitle ( "Mi aplicación OpenGL/GLUT" ); app                                                                                                   . 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 ); return * this ; } FluentGlutApp & withRGBA () { setDisplayMode ( getDisplayMode () | GLUT_RGBA ); return * this ; } FluentGlutApp & withAlpha () { setDisplayMode ( getDisplayMode () | GLUT_ALPHA ); return * this ; } FluentGlutApp & withDepth () { setDisplayMode ( getDisplayMode () | GLUT_DEPTH ); return * this ; } FluentGlutApp & across ( int w , int h ) { setWindowSize ( w , h ); return * this ; } FluentGlutApp & at ( int x , int y ) { setWindowPosition ( x , y ); return * this ; } FluentGlutApp & named ( const char * title ) { setTitle ( title ); return * this ; } // No tiene sentido encadenar después de create(), así que no devuelvas *this void create () { GlutApp :: create (); } };                                                                                           // Uso fluido int main ( int argc , char ** argv ) { FluentGlutApp ( argc , argv ) . withDoubleBuffer (). withRGBA (). withAlpha (). withDepth () . at ( 200 , 200 ). across ( 500 , 500 ) . named ( "Mi aplicación OpenGL/GLUT" ) . create (); }               

Java

Un ejemplo de una expectativa de prueba fluida en el marco de pruebas jMock es: [1]

mock . espera ( una vez ()). método ( "m" ). con ( o ( stringContains ( "hola" ), stringContains ( "cómo" ) ) );   

La biblioteca jOOQ modela SQL como una API fluida en Java:

Autor autor = AUTOR . as ( "autor" ); crear . selectFrom ( autor ) . where ( existe ( selectOne () . from ( LIBRO ) . where ( LIBRO . ESTADO . eq ( ESTADO_LIBRO . AGOTADO )) . y ( LIBRO . ID_AUTOR . eq ( autor . ID ))));       

El procesador de anotaciones fluflu permite la creación de una API fluida utilizando anotaciones Java.

La biblioteca JaQue permite representar las Lambdas de Java 8 como objetos en forma de árboles de expresión en tiempo de ejecución, lo que hace posible crear interfaces fluidas y seguras en cuanto a tipos, es decir, en lugar de:

Cliente obj = ... obj . propiedad ( "nombre" ). eq ( "Juan" )   

Se puede escribir:

método < Cliente > ( cliente -> cliente . getName () == "Juan" )    

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 ( Collection . class ); EasyMock . expect ( mockCollection . remove ( null )) . andThrow ( new NullPointerException ()) . atLeastOnce ();       

En la API de Java Swing, la interfaz LayoutManager define cómo los objetos Container pueden tener una ubicación controlada de los componentes. Una de las LayoutManagerimplementaciones más potentes es la clase GridBagLayout, que requiere el uso de la GridBagConstraintsclase para especificar cómo se produce el control de diseño. Un ejemplo típico del uso de esta clase es algo como lo siguiente.

GridBagLayout gl = nuevo GridBagLayout (); JPanel p = nuevo JPanel () ; p.setLayout ( gl ) ;          JLabel l = new JLabel ( "Nombre:" ); JTextField nm = new JTextField ( 10 );        GridBagConstraints gc = new GridBagConstraints ( ) ; gc.gridx = 0 ; gc.gridy = 0 ; gc.fill = GridBagConstraints.NONE ; p.add ( l , gc ) ;             gc . gridx = 1 ; gc . fill = GridBagConstraints . HORIZONTAL ; gc . weightx = 1 ; p . add ( nm , gc );         

Esto genera una gran cantidad de código y dificulta ver qué está sucediendo exactamente aquí. La Packerclase proporciona un mecanismo fluido, por lo que en su lugar debería escribir: [2]

JPanel p = nuevo JPanel (); Packer pk = nuevo Packer ( p );          JLabel l = new JLabel ( "Nombre:" ); JTextField nm = new JTextField ( 10 );        pk . paquete ( l ). cuadrícula ( 0 ). cuadrícula ( 0 ); pk . paquete ( nm ). cuadrícula ( 1 ). cuadrícula ( 0 ). relleno ();    

Hay muchos lugares donde las API fluidas pueden simplificar la forma en que se escribe el software y ayudar a crear un lenguaje de 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 acciones posteriores en ese contexto.

JavaScript

Existen muchos ejemplos de bibliotecas de JavaScript que utilizan alguna variante de esto, siendo jQuery probablemente la más conocida. Normalmente, los constructores fluidos se utilizan para implementar "consultas de bases de datos", por ejemplo, en la biblioteca cliente Dynamite:

// obtener un elemento de una tabla client . getItem ( 'user-table' ) . setHashKey ( 'userId' , 'userA' ) . setRangeKey ( 'column' , '@' ) .execute () . then ( function ( data ) { // data.result: el objeto resultante })         

Una forma sencilla de hacer esto en JavaScript es utilizando la herencia de prototipos y this.

// Ejemplo de https://schier.co/blog/2013/11/14/method-chaining-in-javascript.htmlclase Gatito { constructor ( ) { this.nombre = 'Garfield ' ; this.color = ' naranja ' ; }            setName ( nombre ) { this . name = nombre ; devolver this ; }        setColor ( color ) { this.color = color ; return this ; }        guardar ( ) { console.log ( `guardando $ { this.name } , el gatito $ { this.color } ` ) ; devolver este ; } }       // úsalo nuevo Kitten () . setName ( 'Salem' ) . setColor ( 'negro' ) . save ();    

Escala

Scala admite una sintaxis fluida tanto para llamadas de métodos como para combinaciones de clases , utilizando rasgos y la withpalabra clave. Por ejemplo:

clase Color { def rgb (): Tuple3 [ Decimal ] } objeto Negro extiende Color { anular def rgb (): Tuple3 [ Decimal ] = ( "0" , "0" , "0" ); }                   rasgo GUIWindow { // Métodos de representación 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 (): this.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 }           rasgo WindowBorder extiende GUIWindow { def render (): GUIWindow = { super.render () . move_to ( arriba_izquierda ( )) . set_pen_color ( Negro ) .line_to ( arriba_derecha ( )) . line_to ( abajo_derecha ()) . line_to ( abajo_izquierda ()) . line_to ( arriba_izquierda ( )) } }                 clase SwingWindow extiende GUIWindow { ... }      val appWin = new SwingWindow ( ) con WindowBorder appWin.render ( )      

Raku

En Raku , existen muchos enfoques, pero uno de los más simples es declarar atributos como de lectura/escritura y usar la givenpalabra clave. Las anotaciones de tipo son opcionales, pero la tipificación gradual nativa hace que sea mucho más seguro escribir directamente en atributos públicos.

clase  Empleado { subconjunto  Salario  de  Real  donde * > 0 ; subconjunto  CadenaNonEmpacada  de  Str  donde * ~~ /\S/ ; # al menos un carácter que no sea un 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 }}mi  $empleado = Empleado . new ();dado  $empleado { . nombre = 'Sally' ; . apellido = 'Ride' ; . salario = 200 ;}diga  $empleado ;# Salida: # Nombre: Sally # Apellido: Ride # Salario: 200

PHP

En PHP , se puede devolver el objeto actual mediante una $thisvariable especial que representa la instancia. Por lo tanto, return $this;el método devolverá la instancia. El siguiente ejemplo define una clase Employeey tres métodos para establecer su nombre, apellido y salario. Cada uno devuelve la instancia de la Employeeclase, lo que permite encadenar métodos.

clase  Empleado {  cadena privada  $nombre ; cadena privada $apellido ; cadena privada $salario ;         función  pública setName ( cadena  $nombre )  {  $this -> nombre  =  $nombre ; devuelve  $this ;  }  función  pública setApellido ( string  $apellido )  {  $this -> apellido  =  $apellido ; devuelve  $this ;  }  función  pública setSalary ( cadena  $salario )  {  $this -> salario  =  $salario ; devuelve  $this ;  }  función  pública __toString ()  {  $employeeInfo  =  'Nombre: '  .  $this -> nombre  .  PHP_EOL ;  $employeeInfo  .=  'Apellido: '  .  $this -> apellido  .  PHP_EOL ;  $employeeInfo  .=  'Salario: '  .  $this -> salario  .  PHP_EOL ; devolver  $employeeInfo ;  } }# Crea una nueva instancia de la clase Empleado, 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  $employee ;# Pantalla: # Nombre: Tom # Apellido: Smith # Salario: 100

Pitón

En Python , regresar selfal método de instancia es una forma de implementar el patrón fluido.

Sin embargo, el creador del lenguaje, Guido van Rossum, [3] lo desaconseja y, por lo tanto, lo considera no pitónico (no idiomático) para operaciones que no devuelven nuevos valores. Van Rossum proporciona operaciones de procesamiento de cadenas como ejemplo en las que considera que el patrón fluido es apropiado.

clase  Poema :  def  __init __ ( self ,  título :  str )  - >  Ninguno :  self.título = título   def  indent ( self ,  spaces :  int ): """Indentar el poema con la cantidad de espacios especificada.""" self . title = " " * spaces + self . title return self           def  suffix ( self ,  author :  str ): " " "Agrega el nombre del autor como sufijo al poema.""" self.title = f " { self.title } - { author } " return self      
>>> Poema ( "Camino no transitado" ) . sangría ( 4 ) . sufijo ( "Robert Frost" ) . título ' Camino no transitado - Robert Frost'

Rápido

En Swift 3.0+, regresar selfen las funciones es una forma de implementar el patrón fluido.

clase  Persona  {  var  nombre :  String  =  ""  var  apellido :  String  =  ""  var  citafavorita :  String  =  "" @ discardableResult  func  set ( nombre :  String )  - >  Self  {  self.firstname = nombre return self }      @ discardableResult  func  set ( apellido :  String )  ->  Self  {  self . lastname  =  apellido  return  self  } @ discardableResult  func  set ( favoriteQuote :  String )  - >  Self  {  self.favoriteQuote = favoriteQuote return self } }     
deje que  persona  =  Persona ()  . set ( nombre :  "Juan" )  . set ( apellido :  "Doe" )  . set ( citafavorita :  "Me gustan las tortugas" )

Inmutabilidad

Es posible crear interfaces fluidas e inmutables que utilicen 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, se clona el objeto, se modifican las propiedades del objeto clonado y se devuelve ese objeto.

La ventaja de este enfoque es que la interfaz se puede utilizar para crear configuraciones de objetos que pueden bifurcarse desde un punto particular, lo que permite 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 ( ) { this.nombre = 'Garfield ' ; this.color = ' naranja ' ; }            setName ( nombre ) { const copia = new Kitten ( ) ; copia.color = this.color ; copia.nombre = nombre ; return copia ; }                setColor ( color ) { const copia = new Kitten ( ) ; copia.nombre = this.nombre ; copia.color = color ; return copia ; }                // ... }// úsalo const kitten1 = new Kitten () .setName ( ' Salem' );     const kitten2 = kitten1 .setColor ( ' negro ' );    console .log ( gatito1 , gatito2 ); // -> Gatito({ nombre: 'Salem', color: 'naranja' }), Gatito({ nombre: 'Salem', color: 'negro' } ) 

Problemas

Los errores no se pueden capturar en tiempo de compilación

En lenguajes tipados, el uso de un constructor que requiere todos los parámetros fallará en el momento de la compilación, mientras que el enfoque fluido solo podrá generar errores en tiempo de ejecución , lo que omitirá todas las comprobaciones de seguridad de tipos de los compiladores modernos. También contradice el enfoque de " fail-fast " para la protección contra errores.

Depuración y notificación de errores

Las sentencias encadenadas de una sola línea pueden resultar más difíciles de depurar, ya que los depuradores pueden no poder establecer puntos de interrupción dentro de la cadena. Recorrer una sentencia de una sola línea en un depurador también puede resultar menos conveniente.

java . nio . ByteBuffer . allocate ( 10 ). rewind ( ). limit ( 100 );

Otro problema es que puede que no quede claro cuál de las llamadas de método causó una excepción, en particular si hay varias llamadas al mismo método. Estos problemas se pueden solucionar dividiendo la declaración en varias líneas, lo que preserva la legibilidad y permite al usuario establecer puntos de interrupción dentro de la cadena y recorrer fácilmente el código línea por línea:

java . nio . ByteBuffer . asignar ( 10 ) . rebobinar () . limitar ( 100 );   

Sin embargo, algunos depuradores siempre muestran la primera línea en el seguimiento de la excepción, aunque la excepción se haya lanzado en cualquier línea.

Explotación florestal

Agregar un registro en medio de una cadena de llamadas fluidas puede ser un problema. Por ejemplo, dado lo siguiente:

ByteBuffer buffer = ByteBuffer .allocate ( 10 ) .rewind ( ) .limit ( 100 ) ;   

Para registrar el estado bufferdespués de la rewind()llamada al método, es necesario interrumpir las llamadas fluidas:

ByteBuffer buffer = ByteBuffer . allocate ( 10 ). rewind (); log . debug ( "El primer byte después de rebobinar es " + buffer . get ( 0 )); buffer . limit ( 100 );     

Esto se puede solucionar en lenguajes que admiten métodos de extensión definiendo una nueva extensión para encapsular la funcionalidad de registro deseada, por ejemplo en C# (usando el mismo ejemplo de Java ByteBuffer que el anterior):

clase estática ByteBufferExtensions { public static ByteBuffer Log ( este búfer ByteBuffer , Log log , Action < ByteBuffer > getMessage ) { string mensaje = getMessage ( búfer ); log . debug ( mensaje ); búfer de retorno ; } }                      // Uso: ByteBuffer . Allocate ( 10 ) . Rewind () . Log ( log , b => "El primer byte después del rebobinado es " + b . Get ( 0 ) ) . Limit ( 100 );           

Subclases

Las subclases en lenguajes fuertemente tipados (C++, Java, C#, etc.) a menudo tienen que anular todos los métodos de su superclase que participan en una interfaz fluida para poder cambiar su tipo de retorno. Por ejemplo:

clase  A { public A doThis () { ... } } clase B extiende A { public B doThis () { super . doThis (); return this ; } // Debe cambiar el tipo de retorno a B. public B doThat () { ... } } ... A a = new B (). doThat (). doThis (); // Esto funcionaría incluso sin anular A.doThis(). B b = new B (). doThis (). doThat (); // Esto fallaría si A.doThis() no se anulara.                                   

Los lenguajes que son capaces de expresar polimorfismo ligado a F pueden utilizarlo para evitar esta dificultad. Por ejemplo:

clase abstracta AbstractA < T extiende AbstractA < T >> { @SuppressWarnings ( "sin marcar" ) público T doThis () { ...; devuelve ( T ) esto ; } } clase A extiende AbstractA < A > {} clase B extiende AbstractA < B > { público B doThat () { ...; devuelve esto ; } }                           ... B b = new B (). doThis (). doThat (); // ¡Funciona! A a = new A (). doThis (); // También funciona.          

Tenga en cuenta que para poder crear instancias de la clase padre, tuvimos que dividirla en dos clases: AbstractAy A, esta última sin contenido (solo contendría constructores si fueran necesarios). El enfoque se puede extender fácilmente si también queremos tener subclases (etc.):

clase abstracta AbstractB < T extiende AbstractB < T >> extiende AbstractA < T > { @SuppressWarnings ( "sin marcar" ) público T doThat () { ...; return ( T ) this ; } } clase B extiende AbstractB < B > {}                  clase abstracta AbstractC < T extiende AbstractC < T >> extiende AbstractB < T > { @SuppressWarnings ( "sin marcar" ) público T foo () { ...; devolver ( T ) esto ; } } clase C extiende AbstractC < C > {} ... C c = new C (). doThis (). doThat (). foo (); // ¡Funciona! B b = new B (). doThis (). doThat (); // Todavía funciona.                            

En un lenguaje con tipado dependiente, por ejemplo Scala, los métodos también se pueden definir explícitamente como que siempre retornan thisy, por lo tanto, se pueden definir solo una vez para que las subclases aprovechen la interfaz fluida:

clase A { def doThis (): this . type = { ... } // devuelve esto, y siempre esto. } clase B extiende A { // ¡No se necesita anulación! def doThat (): this . type = { ... } } ... val a : A = new B (). doThat (). doThis (); // El encadenamiento funciona en ambas direcciones. val b : B = new B (). doThis (). doThat (); // ¡Y ambas cadenas de métodos dan como resultado una B!                                 

Véase también

Referencias

  1. ^ abc Martin Fowler , "FluentInterface", 20 de diciembre de 2005
  2. ^ "Interface Pack200.Packer". Oracle . Consultado el 13 de noviembre de 2019 .
  3. ^ Rossum, Guido van (17 de octubre de 2003). «[Python-Dev] Valor de retorno de sort()» . Consultado el 1 de febrero de 2022 .

Enlaces externos