oop - sintaxis - Método de encadenamiento: ¿por qué es una buena práctica o no?
pruebas gherkin (15)
Creo que la falacia principal es pensar que este es un enfoque orientado a objetos en general cuando en realidad es más un enfoque de programación funcional que otra cosa.
Las principales razones por las que lo uso son tanto para la legibilidad como para evitar que mi código sea inundado por variables.
Realmente no entiendo de lo que otros están hablando cuando dicen que daña la legibilidad. Es una de las formas de programación más concisas y cohesivas que he usado.
También esto:
convertTextToVoice.LoadText ("source.txt"). ConvertToVoice ("destination.wav");
es cómo normalmente lo usaría. Usarlo para encadenar x número de parámetros no es como lo uso habitualmente. Si quisiera poner x parámetros en una llamada a un método, usaría la sintaxis de params :
public void foo (objetos params [] elementos)
Y eche los objetos según el tipo o simplemente use una matriz o colección de tipos de datos según su caso de uso.
El método de encadenamiento es la práctica de métodos de objetos que devuelven el objeto en sí para que se llame al resultado para otro método. Me gusta esto:
participant.addSchedule(events[1]).addSchedule(events[2]).setStatus(''attending'').save()
Esto parece ser una buena práctica, ya que produce código legible o una "interfaz fluida". Sin embargo, para mí, en cambio, parece romper la notación de invocación de objeto implícita en la orientación del objeto en sí: el código resultante no representa acciones de ejecución al resultado de un método anterior, que es cómo se espera que funcione generalmente el código orientado a objetos:
participant.getSchedule(''monday'').saveTo(''monnday.file'')
Esta diferencia logra crear dos significados diferentes para la notación de puntos de "llamar al objeto resultante": en el contexto de encadenamiento, el ejemplo anterior se leería como guardar el objeto participante , aunque el ejemplo está destinado a guardar el programa. objeto recibido por getSchedule.
Entiendo que la diferencia aquí es si se debería esperar que el método llamado devuelva algo o no (en cuyo caso devolvería el objeto llamado para el encadenamiento). Pero estos dos casos no son distinguibles de la notación en sí, solo de la semántica de los métodos que se llaman. Cuando no se usa el método de encadenamiento, siempre puedo saber que una llamada a método opera sobre algo relacionado con el resultado de la llamada anterior: con el encadenamiento, esta asunción se rompe, y tengo que procesar semánticamente toda la cadena para entender cuál es el objeto real. llamado realmente es. Por ejemplo:
participant.attend(event).setNotifications(''silent'').getSocialStream(''twitter'').postStatus(''Joining ''+event.name).follow(event.getSocialId(''twitter''))
Las últimas dos llamadas a métodos se refieren al resultado de getSocialStream, mientras que las anteriores se refieren al participante. Tal vez es una mala práctica escribir cadenas donde el contexto cambia (¿verdad?), Pero incluso así deberá comprobar constantemente si las cadenas de puntos que se ven similares se mantienen en el mismo contexto, o solo funcionan en el resultado .
Para mí, parece que si bien el método de encadenamiento superficial produce código legible, la sobrecarga del significado de la notación de puntos solo genera más confusión. Como no me considero un gurú de la programación, asumo que la culpa es mía. Entonces: ¿Qué me estoy perdiendo? ¿Entiendo que el método de encadenamiento de alguna manera está mal? ¿Hay algún caso en el que el encadenamiento de métodos sea especialmente bueno o en algunos casos sea especialmente malo?
Nota: Entiendo que esta pregunta podría leerse como una declaración de opinión enmascarada como una pregunta. Sin embargo, no lo es: genuinamente quiero entender por qué el encadenamiento se considera una buena práctica, y dónde me equivoco al pensar que rompe la notación inherente orientada a objetos.
El bueno:
- Es escueto, pero le permite poner más en una sola línea de manera elegante.
- A veces puede evitar el uso de una variable, que ocasionalmente puede ser útil.
- Puede funcionar mejor.
El malo:
- Está implementando devoluciones, básicamente agregando funcionalidad a métodos en objetos que realmente no forman parte de lo que esos métodos deben hacer. Devuelve algo que ya tiene solo para guardar unos pocos bytes.
- Oculta los cambios de contexto cuando una cadena lleva a otra. Puede obtener esto con getters, excepto que es bastante claro cuando cambia el contexto.
- El encadenamiento de varias líneas se ve feo, no funciona bien con la sangría y puede causar confusión en el manejo de algunos operadores (especialmente en lenguajes con ASI).
- Si desea comenzar a devolver algo más que sea útil para un método encadenado, es probable que le cueste más solucionarlo o tener más problemas con él.
- Estás descargando el control a una entidad a la que normalmente no descargarías por pura conveniencia, incluso en lenguajes estrictamente tipados no siempre se pueden detectar los errores causados por esto.
- Puede funcionar peor.
General:
Un buen enfoque es no utilizar el encadenamiento en general hasta que surjan situaciones o módulos específicos sean particularmente adecuados para ello.
El encadenamiento puede perjudicar la legibilidad bastante severamente en algunos casos, especialmente cuando se pesa en el punto 1 y 2.
En caso de acuse de recibo, puede ser mal utilizado, como en lugar de otro método (por ejemplo, pasar una matriz) o mezclar métodos de maneras extrañas (parent.setSomething (). GetChild (). SetSomething (). GetParent (). SetSomething ()).
El encadenamiento de métodos puede permitir el diseño de DSLs avanzados en Java directamente. En esencia, puede modelar al menos estos tipos de reglas de DSL:
1. SINGLE-WORD
2. PARAMETERISED-WORD parameter
3. WORD1 [ OPTIONAL-WORD]
4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B }
5. WORD3 [ , WORD3 ... ]
Estas reglas se pueden implementar usando estas interfaces
// Initial interface, entry point of the DSL
interface Start {
End singleWord();
End parameterisedWord(String parameter);
Intermediate1 word1();
Intermediate2 word2();
Intermediate3 word3();
}
// Terminating interface, might also contain methods like execute();
interface End {}
// Intermediate DSL "step" extending the interface that is returned
// by optionalWord(), to make that method "optional"
interface Intermediate1 extends End {
End optionalWord();
}
// Intermediate DSL "step" providing several choices (similar to Start)
interface Intermediate2 {
End wordChoiceA();
End wordChoiceB();
}
// Intermediate interface returning itself on word3(), in order to allow for
// repetitions. Repetitions can be ended any time because this interface
// extends End
interface Intermediate3 extends End {
Intermediate3 word3();
}
Con estas simples reglas, puede implementar DSL complejas como SQL directamente en Java, como lo hace jOOQ , una biblioteca que he creado. Vea un ejemplo SQL bastante complejo tomado de mi blog aquí:
create().select(
r1.ROUTINE_NAME,
r1.SPECIFIC_NAME,
decode()
.when(exists(create()
.selectOne()
.from(PARAMETERS)
.where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA))
.and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME))
.and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))),
val("void"))
.otherwise(r1.DATA_TYPE).as("data_type"),
r1.NUMERIC_PRECISION,
r1.NUMERIC_SCALE,
r1.TYPE_UDT_NAME,
decode().when(
exists(
create().selectOne()
.from(r2)
.where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
.and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
.and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))),
create().select(count())
.from(r2)
.where(r2.ROUTINE_SCHEMA.equal(getSchemaName()))
.and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME))
.and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField())
.as("overload"))
.from(r1)
.where(r1.ROUTINE_SCHEMA.equal(getSchemaName()))
.orderBy(r1.ROUTINE_NAME.asc())
.fetch()
Otro buen ejemplo es jRTF , un pequeño DSL diseñado para cementar documentos RTF directamente en Java. Un ejemplo:
rtf()
.header(
color( 0xff, 0, 0 ).at( 0 ),
color( 0, 0xff, 0 ).at( 1 ),
color( 0, 0, 0xff ).at( 2 ),
font( "Calibri" ).at( 0 ) )
.section(
p( font( 1, "Second paragraph" ) ),
p( color( 1, "green" ) )
)
).out( out );
El método de encadenamiento puede ser simplemente una novedad para la mayoría de los casos, pero creo que tiene su lugar. Un ejemplo podría encontrarse en el uso del registro activo de CodeIgniter :
$this->db->select(''something'')->from(''table'')->where(''id'', $id);
Eso se ve mucho más limpio (y tiene más sentido, en mi opinión) que:
$this->db->select(''something'');
$this->db->from(''table'');
$this->db->where(''id'', $id);
Realmente es subjetivo; Todos tienen su propia opinión.
El punto totalmente olvidado aquí, es que el método de encadenamiento permite DRY . Es un sustituto eficaz para el "con" (que está mal implementado en algunos idiomas).
A.method1().method2().method3(); // one A
A.method1();
A.method2();
A.method3(); // repeating A 3 times
Esto importa por la misma razón DRY siempre importa; si A resulta ser un error, y estas operaciones deben realizarse en B, solo necesita actualizar en 1 lugar, no en 3.
Pragmáticamente, la ventaja es pequeña en este caso. Aún así, un poco menos de tipeo, un poco más robusto (DRY), lo tomaré.
En mi opinión, el encadenamiento de métodos es un poco novedoso. Claro, parece genial, pero no veo ninguna ventaja real en eso.
Como es:
someList.addObject("str1").addObject("str2").addObject("str3")
cualquier mejor que:
someList.addObject("str1")
someList.addObject("str2")
someList.addObject("str3")
La excepción podría ser cuando addObject () devuelve un nuevo objeto, en cuyo caso el código liberado puede ser un poco más engorroso como:
someList = someList.addObject("str1")
someList = someList.addObject("str2")
someList = someList.addObject("str3")
Es peligroso porque puede depender de más objetos de los esperados, como entonces su llamada devuelve una instancia de otra clase:
Daré un ejemplo:
foodStore es un objeto que se compone de muchas tiendas de alimentos que usted posee. foodstore.getLocalStore () devuelve un objeto que contiene información en la tienda más cercana al parámetro. getPriceforProduct (cualquier cosa) es un método de ese objeto.
Entonces cuando llamas a foodStore.getLocalStore (parameters) .getPriceforProduct (cualquier cosa)
no solo depende de FoodStore, sino también de LocalStore.
Si getPriceforProduct (cualquier cosa) cambia alguna vez, debe cambiar no solo FoodStore sino también la clase que llamó al método encadenado.
Siempre debe aspirar a un acoplamiento flexible entre las clases.
Dicho esto, personalmente me gusta encadenarlos cuando programo Ruby.
Esto parece un poco subjetivo.
El método de encadenamiento no es algo inherentemente malo o bueno.
La legibilidad es lo más importante.
(También considere que tener un gran número de métodos encadenados hará que las cosas sean muy frágiles si algo cambia)
Estoy de acuerdo en que esto es subjetivo. En general, evito el encadenamiento de métodos, pero recientemente también encontré un caso en el que era lo correcto: tenía un método que aceptaba algo así como 10 parámetros y necesitaba más, pero durante la mayor parte del tiempo solo tenía que especificar un pocos. Con anulaciones, esto se volvió muy engorroso muy rápido. En cambio, opté por el enfoque de encadenamiento:
MyObject.Start()
.SpecifySomeParameter(asdasd)
.SpecifySomeOtherParameter(asdasd)
.Execute();
Esto es algo así como un patrón de fábrica. El método de encadenamiento de métodos era opcional, pero facilitaba el código de escritura (especialmente con IntelliSense). Sin embargo, tenga en cuenta que este es un caso aislado, y no es una práctica general en mi código.
El punto es que en el 99% de los casos, probablemente puedas hacerlo igual de bien o incluso mejor sin un método de encadenamiento. Pero está el 1% donde este es el mejor enfoque.
Estoy de acuerdo, por lo tanto, cambié la forma en que se implementó una interfaz fluida en mi biblioteca.
Antes de:
collection.orderBy("column").limit(10);
Después:
collection = collection.orderBy("column").limit(10);
En la implementación "antes", las funciones modificaban el objeto y terminaban con return this
. Cambié la implementación para devolver un nuevo objeto del mismo tipo .
Mi razonamiento para este cambio :
El valor de retorno no tenía nada que ver con la función, era puramente para apoyar la parte de encadenamiento, debería haber sido una función nula de acuerdo con OOP.
El encadenamiento de métodos en las bibliotecas del sistema también lo implementa de esa manera (como linq o string):
myText = myText.trim().toUpperCase();
El objeto original permanece intacto, lo que permite al usuario API decidir qué hacer con él. Permite:
page1 = collection.limit(10); page2 = collection.offset(10).limit(10);
Una implementación de copia también se puede usar para construir objetos:
painting = canvas.withBackground(''white'').withPenSize(10);
Donde la función
setBackground(color)
cambia la instancia y no devuelve nada (como se supone que debe) .El comportamiento de las funciones es más predecible (Ver punto 1 y 2).
Usar un nombre corto de variable también puede reducir el desorden de código, sin forzar una API en el modelo.
var p = participant; // create a reference p.addSchedule(events[1]);p.addSchedule(events[2]);p.setStatus(''attending'');p.save()
Conclusión:
En mi opinión, una interfaz fluida que utiliza una implementación de return this
es simplemente incorrecta.
Martin Fowler tiene una buena discusión aquí:
Cuándo usarlo
Método El encadenamiento puede agregar mucho a la legibilidad de una DSL interna y, como resultado, se ha convertido casi en un sinónimo de DSL internas en algunas mentes. Sin embargo, el método de encadenamiento es el mejor cuando se usa junto con otras combinaciones de funciones.
Método El encadenamiento es particularmente efectivo con gramáticas como parent :: = (this | that) *. El uso de diferentes métodos proporciona una forma legible de ver qué argumento viene a continuación. De forma similar, los argumentos opcionales se pueden omitir fácilmente con Method Chaining. Una lista de cláusulas obligatorias, como parent :: = first second no funciona tan bien con la forma básica, aunque puede ser soportada usando interfaces progresivas. La mayoría de las veces prefiero la función anidada para ese caso.
El mayor problema para Method Chaining es el problema final. Si bien hay soluciones alternativas, por lo general, si te topas con esto, es mejor utilizar una función anidada. La función anidada también es una mejor opción si te metes en un lío con las variables de contexto.
Muchos usan el método de encadenamiento como una forma de conveniencia en lugar de tener preocupaciones de legibilidad en mente. El encadenamiento de métodos es aceptable si implica realizar la misma acción en el mismo objeto, pero solo si realmente mejora la legibilidad, y no solo para escribir menos código.
Desafortunadamente, muchos usan el método de encadenamiento según los ejemplos dados en la pregunta. Si bien aún pueden hacerse legibles, desafortunadamente causan un alto acoplamiento entre múltiples clases, por lo que no es deseable.
Personalmente, prefiero los métodos de encadenamiento que solo actúan sobre el objeto original, por ejemplo, configurar propiedades múltiples o llamar a métodos de tipo utilidad.
foo.setHeight(100).setWidth(50).setColor(''#ffffff'');
foo.moveTo(100,100).highlight();
No lo uso cuando uno o más de los métodos encadenados devuelvan cualquier objeto que no sea foo en mi ejemplo. Aunque sintácticamente puede encadenar cualquier cosa siempre que use la API correcta para ese objeto en la cadena, el cambio de objetos en mi humilde opinión hace que las cosas sean menos legibles y puede ser realmente confuso si las API para los diferentes objetos tienen alguna similitud. Si haces alguna llamada al método realmente común al final ( .toString()
, .print()
, whatever) ¿a qué objeto estás actuando en última instancia? Alguien que lea casualmente el código podría no detectar que sería un objeto devuelto implícitamente en la cadena en lugar de la referencia original.
Encadenar diferentes objetos también puede conducir a errores nulos inesperados. En mis ejemplos, suponiendo que foo es válido, todas las llamadas a métodos son "seguras" (p. Ej., Válidas para foo). En el ejemplo de OP:
participant.getSchedule(''monday'').saveTo(''monnday.file'')
... no hay garantía (como un desarrollador externo que mira el código) de que getSchedule realmente devuelva un objeto de horario válido, no nulo. Además, la depuración de este estilo de código a menudo es mucho más difícil ya que muchos IDE no evaluarán la llamada al método en el momento de la depuración como un objeto que puede inspeccionar. IMO, cada vez que necesites un objeto para inspeccionar con fines de depuración, prefiero tenerlo en una variable explícita.
Solo mis 2 centavos;
El método de encadenamiento hace que la depuración sea difícil: - No puede poner el punto de interrupción en un punto conciso para que pueda pausar el programa exactamente donde lo desee. Si uno de estos métodos arroja una excepción y obtiene un número de línea, no tiene idea qué método en la "cadena" causó el problema.
Creo que generalmente es una buena práctica escribir siempre líneas cortas y concisas. Cada línea solo debe hacer una llamada a un método. Prefiere más líneas a líneas más largas.
EDITAR: el comentario menciona que el método de encadenamiento y salto de línea están separados. Eso es verdad. Sin embargo, dependiendo del depurador, puede o no ser posible colocar un punto de interrupción en el medio de una declaración. Incluso si puede, el uso de líneas separadas con variables intermedias le brinda mucha más flexibilidad y un montón de valores que puede examinar en la ventana Inspección que ayuda al proceso de depuración.
Beneficios del encadenamiento
es decir, donde me gusta usarlo
Un beneficio de encadenar que no vi mencionado fue la capacidad de usarlo durante la iniciación variable, o al pasar un nuevo objeto a un método, no estoy seguro si esto es una mala práctica o no.
Sé que este es un ejemplo artificial, pero digamos que tienes las siguientes clases
Public Class Location
Private _x As Integer = 15
Private _y As Integer = 421513
Public Function X() As Integer
Return _x
End Function
Public Function X(ByVal value As Integer) As Location
_x = value
Return Me
End Function
Public Function Y() As Integer
Return _y
End Function
Public Function Y(ByVal value As Integer) As Location
_y = value
Return Me
End Function
Public Overrides Function toString() As String
Return String.Format("{0},{1}", _x, _y)
End Function
End Class
Public Class HomeLocation
Inherits Location
Public Overrides Function toString() As String
Return String.Format("Home Is at: {0},{1}", X(), Y())
End Function
End Class
Y decir que no tiene acceso a la clase base, o decir que los valores predeterminados son dinámicos, basados en la hora, etc. Sí, podría crear instancias y luego cambiar los valores, pero eso puede volverse engorroso, especialmente si solo está pasando los valores a un método:
Dim loc As New HomeLocation()
loc.X(1337)
PrintLocation(loc)
Pero esto no es mucho más fácil de leer:
PrintLocation(New HomeLocation().X(1337))
O, ¿qué tal un miembro de la clase?
Public Class Dummy
Private _locA As New Location()
Public Sub New()
_locA.X(1337)
End Sub
End Class
vs
Public Class Dummy
Private _locC As Location = New Location().X(1337)
End Class
Así es como he estado utilizando el encadenamiento, y normalmente mis métodos son solo para la configuración, por lo que tienen solo 2 líneas de longitud, establecen un valor y luego Return Me
. Para nosotros ha limpiado enormes líneas muy difíciles de leer y comprender el código en una línea que se lee como una oración. algo como
New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats
Vs Algo así como
New Dealer.CarPicker(Dealer.CarPicker.Makes.Subaru
, Dealer.CarPicker.Models.WRX
, Dealer.CarPicker.Transmissions.SixSpeed
, Dealer.CarPicker.Engine.Options.TurboCharged
, Dealer.CarPicker.Exterior.Color.Blue
, Dealer.CarPicker.Interior.Color.Gray
, Dealer.CarPicker.Interior.Options.Leather
, Dealer.CarPicker.Interior.Seats.Heated)
Detrimento de encadenar
es decir, donde no me gusta usarlo
No uso el encadenamiento cuando hay muchos parámetros para pasar a las rutinas, principalmente porque las líneas son muy largas, y como el OP mencionó, puede ser confuso cuando llamas rutinas a otras clases para pasar a una de los métodos de encadenamiento
También existe la preocupación de que una rutina devuelva datos no válidos, por lo que hasta ahora solo he utilizado el encadenamiento cuando devuelvo la misma instancia que se llama. Como se señaló, si encadena entre clases, la depuración será más difícil (¿cuál devolvió nulo?) Y puede aumentar el acoplamiento de dependencias entre clases.
Conclusión
Como todo en la vida y la programación, el encadenamiento no es bueno ni malo si puedes evitar lo malo, entonces encadenar puede ser un gran beneficio.
Intento seguir estas reglas
- Intenta no encadenar entre clases
- Haga rutinas específicamente para encadenar
- Haga solo UNA cosa en una rutina de encadenamiento
- Úselo cuando mejore la legibilidad
- Úselo cuando hace que el código sea más simple