¿No son malas también las "jerarquías de composición profunda"?


19

Disculpas si "Jerarquía de composición" no es una cosa, pero explicaré a qué me refiero en la pregunta.

No hay ningún programador de OO que no haya encontrado una variación de "Mantener jerarquías de herencia planas" o "Preferir composición sobre herencia", etc. Sin embargo, las jerarquías de composición profunda también parecen problemáticas.

Digamos que necesitamos una colección de informes que detallen los resultados de un experimento:

class Model {
    // ... interface

    Array<Result> m_results;
}

Cada resultado tiene ciertas propiedades. Estos incluyen el tiempo del experimento, así como algunos metadatos de cada etapa del experimento:

enum Stage {
    Pre = 1,
    Post
};

class Result {
    // ... interface

    Epoch m_epoch;
    Map<Stage, ExperimentModules> m_modules; 
}

Vale genial. Ahora, cada módulo de experimento tiene una cadena que describe el resultado del experimento, así como una colección de referencias a conjuntos de muestras experimentales:

class ExperimentalModules {
    // ... interface

    String m_reportText;
    Array<Sample> m_entities;
}

Y luego cada muestra tiene ... bueno, te haces una idea.

El problema es que si estoy modelando objetos del dominio de mi aplicación, esto parece un ajuste muy natural, pero al final del día, un Result es un simple contenedor de datos! No parece que valga la pena crear un gran grupo de clases para ello.

Suponiendo que las estructuras de datos y las clases que se muestran arriba modelan correctamente las relaciones en el dominio de la aplicación, ¿hay una mejor manera de modelar tal "resultado", sin recurrir a una jerarquía de composición profunda? ¿Existe algún contexto externo que lo ayude a determinar si ese diseño es bueno o no?


No entiendo la parte del contexto externo.
Tulains Córdova

@ TulainsCórdova lo que estoy tratando de preguntar aquí es "¿hay situaciones para las que la composición profunda es una buena solución"?
AwesomeSauce

Agregué una respuesta.
Tulains Córdova

55
Sí exactamente. Los problemas que las personas intentan solucionar al reemplazar la herencia con la composición no desaparecen mágicamente solo porque cambiaste una estrategia de agregación de características por otra. Ambas son soluciones para gestionar la complejidad, y esa complejidad todavía está ahí y debe manejarse de alguna manera.
Mason Wheeler

3
No veo nada malo con los modelos de datos profundos. Pero tampoco veo cómo podría haber modelado esto con herencia ya que es una relación 1: N en cada capa.
Usr

Respuestas:


17

De esto se trata la Ley de Demeter / principio del menor conocimiento . Un usuario de la Modelno necesariamente debe saber acerca de ExperimentalModulesla interfaz para hacer su trabajo.

El problema general es que cuando una clase hereda de otro tipo o tiene un campo público con algún tipo o cuando un método toma un tipo como parámetro o devuelve un tipo, las interfaces de todos esos tipos se convierten en parte de la interfaz efectiva de mi clase. La pregunta es: estos tipos de los que dependo: ¿los uso como punto central de mi clase? ¿O son solo detalles de implementación que he expuesto accidentalmente? Porque si son detalles de implementación, los expuse y permití que los usuarios de mi clase dependieran de ellos; mis clientes ahora están estrechamente vinculados a los detalles de mi implementación.

Entonces, si quiero mejorar la cohesión, puedo limitar este acoplamiento escribiendo más métodos que hagan cosas útiles, en lugar de requerir que los usuarios accedan a mis estructuras privadas. Aquí, podemos ofrecer métodos para buscar o filtrar resultados. Eso hace que mi clase sea más fácil de refactorizar, ya que ahora puedo cambiar la representación interna sin un cambio importante de mi interfaz.

Pero esto no está exento de costos: la interfaz de mi clase expuesta ahora se hincha con operaciones que no siempre son necesarias. Esto viola el principio de segregación de interfaz: no debería obligar a los usuarios a depender de más operaciones de las que realmente usan. Y ahí es donde el diseño es importante: el diseño consiste en decidir qué principio es más importante en su contexto y en encontrar un compromiso adecuado.

Al diseñar una biblioteca que debe mantener la compatibilidad de origen en varias versiones, estaría más inclinado a seguir el principio de menor conocimiento con más cuidado. Si mi biblioteca usa otra biblioteca internamente, nunca expondría nada definido por esa biblioteca a mis usuarios. Si necesito exponer una interfaz de la biblioteca interna, crearía un objeto contenedor simple. Los patrones de diseño como el patrón de puente o el patrón de fachada pueden ayudar aquí.

Pero si mis interfaces solo se usan internamente, o si las interfaces que expondría son muy fundamentales (parte de una biblioteca estándar o de una biblioteca tan fundamental que nunca tendría sentido cambiarla), entonces sería inútil esconderse Estas interfaces. Como de costumbre, no hay respuestas únicas para todos.


Mencionaste una gran preocupación que estaba teniendo; La idea de que tenía que llegar muy lejos en la implementación para hacer algo útil en los niveles superiores de abstracción. Una discusión muy útil, gracias.
AwesomeSauce

7

¿No son malas también las "jerarquías de composición profunda"?

Por supuesto. La regla debería decir "mantener todas las jerarquías lo más planas posible".

Pero las jerarquías de composición profunda son menos frágiles que las jerarquías de herencia profundas y más flexibles para cambiar. Intuitivamente, esto no debería sorprendernos; Las jerarquías familiares son de la misma manera. Su relación con sus parientes consanguíneos está fijada en genética; sus relaciones con sus amigos y su cónyuge no lo son.

Su ejemplo no me parece una "jerarquía profunda". Una forma hereda de un rectángulo que hereda de un conjunto de puntos que hereda de las coordenadas x e y, y aún no he tenido una cabeza de vapor completa.


4

Parafraseando a Einstein:

mantener todas las jerarquías lo más planas posible, pero no más planas

Es decir, si el problema que está modelando es así, no debería tener reparos en modelarlo tal como es.

Si la aplicación fuera solo una hoja de cálculo gigante, entonces no tiene que modelar la jerarquía de composición tal como está. De lo contrario, hazlo. No creo que la cosa sea infinitamente profunda. Simplemente mantenga a los captadores que devuelven colecciones perezosas si poblarlas es costoso, como pedirlas a un repositorio que las extrae de una base de datos. Por perezoso quiero decir, no los llenes hasta que sean necesarios.


1

Sí, puedes modelarlo como tablas relacionales

Result {
    Id
    ModelId
    Epoch
}

etc. que a menudo puede conducir a una programación más fácil y una asignación simple a una base de datos. pero a costa de perder la codificación dura de tus relaciones


0

Realmente no sé cómo resolver esto en Java, porque Java se basa en la idea de que todo es un objeto. No puede suprimir las clases intermedias reemplazándolas por un diccionario porque los tipos en sus blobs de datos son heterogéneos (como una marca de tiempo + lista o una cadena + lista ...). La alternativa de reemplazar una clase contenedor con su campo (por ejemplo, pasar una variable "m_epoch" por un "m_modules") es simplemente mala, porque está haciendo un objeto implícito (no se puede tener uno sin el otro) sin ningún particular beneficio.

En mi lenguaje de predilección (Python), mi regla general sería no crear un objeto si ninguna lógica particular (excepto la obtención y configuración básicas) le pertenece. Pero dadas sus limitaciones, creo que la jerarquía de composición que tiene es el mejor diseño posible.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.