Método de encadenamiento vs encapsulación

17

Existe el clásico problema OOP de encadenamiento de métodos versus métodos de "punto de acceso único":

main.getA().getB().getC().transmogrify(x, y)

vs

main.getA().transmogrifyMyC(x, y)

La primera parece tener la ventaja de que cada clase solo es responsable de un conjunto más pequeño de operaciones, y hace que todo sea mucho más modular : agregar un método a C no requiere ningún esfuerzo en A, B o C para exponerlo.

La desventaja, por supuesto, es una encapsulación más débil , que resuelve el segundo código. Ahora A tiene el control de cada método que lo atraviesa y puede delegarlo en sus campos si así lo desea.

Me doy cuenta de que no hay una solución única y, por supuesto, depende del contexto, pero realmente me gustaría escuchar algunos comentarios sobre otras diferencias importantes entre los dos estilos, y en qué circunstancias debería preferir cualquiera de ellos, porque ahora, cuando lo intento para diseñar un código, siento que simplemente no estoy usando los argumentos para decidir de una forma u otra.

Roble
fuente

Respuestas:

25

Creo que la Ley de Demeter proporciona una directriz importante en esto (con sus ventajas y desventajas, que, como de costumbre, deben medirse caso por caso).

La ventaja de seguir la Ley de Demeter es que el software resultante tiende a ser más fácil de mantener y adaptar. Dado que los objetos son menos dependientes de la estructura interna de otros objetos, los contenedores de objetos se pueden cambiar sin modificar sus llamadores.

Una desventaja de la Ley de Demeter es que a veces requiere escribir una gran cantidad de pequeños métodos "envoltorios" para propagar las llamadas a los componentes. Además, la interfaz de una clase puede volverse voluminosa ya que aloja métodos para clases contenidas, lo que resulta en una clase sin una interfaz cohesiva. Pero esto también podría ser un signo de mal diseño de OO.

Péter Török
fuente
Olvidé esa ley, gracias por recordármelo. Pero lo que estoy preguntando aquí es principalmente cuáles son las ventajas y desventajas, o más exactamente cómo debo decidir usar un estilo sobre el otro.
Roble
@Oak, agregué citas que describen las ventajas y desventajas.
Péter Török
10

En general, trato de mantener el método de encadenamiento lo más limitado posible (basado en la Ley de Demeter )

La única excepción que hago es para interfaces fluidas / programación interna de estilo DSL.

Martin Fowler hace una especie de distinción en los lenguajes específicos del dominio, pero por razones de violación de la separación de consultas de comandos que establece:

que cada método debe ser un comando que realiza una acción o una consulta que devuelve datos a la persona que llama, pero no ambos.

Fowler en su libro en la página 70 dice:

La separación entre comandos y consultas es un principio extremadamente valioso en la programación, y recomiendo encarecidamente a los equipos que lo usen. Una de las consecuencias de usar el método de encadenamiento en DSL internos es que generalmente rompe este principio: cada método altera el estado pero devuelve un objeto para continuar la cadena. He usado muchos decibelios para menospreciar a las personas que no siguen la separación entre comandos y consultas, y lo volveré a hacer. Pero las interfaces fluidas siguen un conjunto diferente de reglas, así que estoy feliz de permitirlo aquí.

KeesDijk
fuente
3

Creo que la pregunta es si estás usando una abstracción adecuada.

En el primer caso, tenemos

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Donde main es de tipo IHasGetA. La pregunta es: ¿es adecuada esa abstracción? La respuesta no es trivial. Y en este caso parece un poco extraño, pero de todos modos es un ejemplo teórico. Pero para construir un ejemplo diferente:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

A menudo es mejor que

main.superTransmogrify(v, w, x, y, z);

Debido a que en el último ejemplo ambos thisy maindependerá de los tipos de v, w, x, yyz . Además, el código no se ve mucho mejor, si cada declaración de método tiene media docena de argumentos.

Un localizador de servicios realmente requiere el primer enfoque. No desea acceder a la instancia que crea a través del localizador de servicios.

Por lo tanto, "alcanzar" a través de un objeto puede crear mucha dependencia, más aún si se basa en propiedades de clases reales.
Sin embargo, crear una abstracción, que consiste en proporcionar un objeto, es algo completamente diferente.

Por ejemplo, podrías tener:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

¿Dónde mainestá una instancia de Main. Si el conocimiento de la clase mainreduce la dependencia a, en IHasGetAlugar de Main, encontrará que el acoplamiento es bastante bajo. El código de llamada ni siquiera sabe que en realidad está llamando al último método en el objeto original, que en realidad ilustra el grado de desacoplamiento.
Llegas a lo largo de un camino de abstracciones concisas y ortogonales, en lugar de profundizar en lo interno de una implementación.

back2dos
fuente
Punto muy interesante sobre el gran aumento en el número de parámetros.
Roble
2

La Ley de Deméter , como señala @ Péter Török, sugiere la forma "compacta".

Además, cuantos más métodos mencione explícitamente en su código, más clases dependerá de su clase, lo que aumentará los problemas de mantenimiento. En su ejemplo, la forma compacta depende de dos clases, mientras que la forma más larga depende de cuatro clases. La forma más larga no solo contraviene la Ley de Deméter; también le hará cambiar su código cada vez que cambie cualquiera de los cuatro métodos a los que se hace referencia (a diferencia de los dos en forma compacta).

CesarGon
fuente
Por otro lado, seguir ciegamente esa ley significa que la cantidad de métodos Aexplotará y muchos métodos Apueden querer delegar de todos modos. Aún así, estoy de acuerdo con las dependencias, eso reduce drásticamente la cantidad de dependencias requeridas del código del cliente.
Roble
1
@Oak: Hacer algo a ciegas nunca es bueno. Hay que mirar los pros y los contras y tomar decisiones basadas en la evidencia. Esto incluye la Ley de Deméter también.
CesarGon
2

He luchado con este problema yo mismo. La desventaja de "alcanzar" profundamente en diferentes objetos es que cuando realizas la refactorización terminarás teniendo que cambiar una gran cantidad de código ya que hay tantas dependencias. Además, su código se vuelve un poco hinchado y más difícil de leer.

Por otro lado, tener clases que simplemente "transmiten" métodos también significa una sobrecarga de tener que declarar múltiples métodos en más de un lugar.

Una solución que mitiga esto y es apropiada en algunos casos es tener una clase de fábrica que construya una especie de objeto de fachada copiando datos / objetos de las clases apropiadas. De esa manera, puede codificar contra su objeto de fachada y cuando refactoriza simplemente cambia la lógica de la fábrica.

Homde
fuente
1

A menudo encuentro que la lógica de un programa es más fácil de asimilar con métodos encadenados. Para mí, se customer.getLastInvoice().itemCount()adapta mejor a mi cerebro quecustomer.countLastInvoiceItems() .

Depende de usted si vale la pena el dolor de cabeza de mantenimiento de tener el acoplamiento adicional. (También me gustan las funciones pequeñas en clases pequeñas, así que tiendo a encadenar. No digo que sea correcto, es solo lo que hago).

JT Grimes
fuente
eso debería ser IMO customer.NrLastInvoices o customer.LastInvoice.NrItems. Esa cadena no es demasiado larga, por lo que probablemente no valga la pena aplanarse si la cantidad de combinaciones es algo grande
Homde