Jerarquías paralelas: en parte iguales, en parte diferentes

12

Hay bastantes preguntas similares por ahí 1 ,2 ,3 ,4 , pero no parece exactamente el caso en esta pregunta, ni las soluciones parecen óptimas.

Esta es una pregunta general de OOP, suponiendo que el polimorfismo, los genéricos y los mixins estén disponibles. El lenguaje real que se utilizará es OOP Javascript (Typecript), pero es el mismo problema en Java o C ++.

Tengo jerarquías de clases paralelas, que a veces comparten el mismo comportamiento (interfaz e implementación), pero a veces cada uno tiene su propio comportamiento 'protegido'. Ilustrado así:

3 jerarquías de clases paralelas, la columna central muestra las partes comunes, la columna izquierda es la jerarquía del lienzo y la columna derecha muestra la jerarquía SVG

Esto es solo para fines ilustrativos ; No es el diagrama de clase real. Para leerlo:

  • Cualquier cosa en la jerarquía común (centro) se comparte entre las jerarquías Canvas (izquierda) y SVG (derecha). Por compartir me refiero tanto a la interfaz como a la implementación.
  • Cualquier cosa solo en las columnas izquierda o derecha significa un comportamiento (métodos y miembros) específico de esa jerarquía. Por ejemplo:
    • Las jerarquías izquierda y derecha utilizan exactamente los mismos mecanismos de validación, que se muestran como un único método ( Viewee.validate()) en la jerarquía común.
    • Solo la jerarquía del lienzo tiene un método paint(). Este método llama al método de pintura en todos los niños.
    • La jerarquía SVG necesita anular el addChild()método de Composite, pero ese no es el caso con la jerarquía de lienzo.
  • Las construcciones de las dos jerarquías laterales no se pueden mezclar. Una fábrica asegura eso.

Solución I - Herencia aparte

La herencia de Fowler's Tease Apart no parece hacer el trabajo aquí, porque hay alguna discrepancia entre los dos paralelos.

Solución II - Mixins

Este es el único en el que puedo pensar actualmente. Las dos jerarquías se desarrollan por separado, pero en cada nivel las clases se mezclan en la clase común, que no forman parte de una jerarquía de clases. Omitiendo el structuraltenedor, se verá así:

Las tres columnas nuevamente, las columnas izquierda y derecha son jerarquías paralelas, donde cada clase también es inherente a una clase común.  Las clases comunes no son parte de una jerarquía.

Tenga en cuenta que cada columna estará en su propio espacio de nombres, por lo que los nombres de clase no entrarán en conflicto.

La pregunta

¿Alguien puede ver fallas con este enfoque? ¿Alguien puede pensar en una mejor solución?


Apéndice

Aquí hay un código de muestra de cómo se va a usar. El espacio de nombres svgse puede reemplazar con canvas:

var iView        = document.getElementById( 'view' ),
    iKandinsky   = new svg.Kandinsky(),
    iEpigone     = new svg.Epigone(),
    iTonyBlair   = new svg.TonyBlair( iView, iKandinsky ),
    iLayer       = new svg.Layer(),
    iZoomer      = new svg.Zoomer(),
    iFace        = new svg.Rectangle( new Rect( 20, 20, 100, 60) ),
    iEyeL        = new svg.Rectangle( new Rect( 20, 20, 20, 20) ),
    iEyeR        = new svg.Rectangle( new Rect( 60, 20, 20, 20) );

iKandinsky.setContext( iTonyBlair.canvas.getContext( '2d' ) );
iEpigone.setContext( iTonyBlair.canvas.getContext( '2d' ) );

iFace.addChildren( iEyeL, iEyeR );
iZoomer.setZoom( new Point( 2, 2 ) );
iZoomer.addChild( iFace );
iLayer.addChild( iZoomer );
iTonyBlair.setContent( iLayer );

Esencialmente, en los clientes en tiempo de ejecución componen la jerarquía de instancias de las subclases de Viewee; al igual que:

Una imagen que muestra una jerarquía de objetos como capa, rect, desplazamiento, etc.

Digamos que todos estos espectadores son de la jerarquía del lienzo, se representan atravesando la jerarquía que puede llamar paint()a cada espectador. Si son de la jerarquía de svg, los espectadores saben cómo agregarse al DOM, pero no hay paint()recorrido.

Izhaki
fuente
Lectura recomendada: Revisión de diseño: ¿sobre el tema o no?
Robert Harvey
¿Tal vez intente con el patrón de diseño Decorator completo (Erich Gamma et als, Design Patterns)?
Zon
¿Qué es un espectador? ¿Qué significa "paralelo" como sustantivo (en oposición a un adjetivo)?
Tulains Córdova
¿Tienes herencia múltiple?
Tulains Córdova
¿Las clases Canvas o SVG contienen estados o datos adicionales que no están en común? ¿Cómo usas las clases? ¿Puedes mostrar algún código de ejemplo que muestre cómo podrían usarse estas jerarquías?
Eufórico el

Respuestas:

5

El segundo enfoque segrega mejor las interfaces, siguiendo el principio de segregación de interfaz.

Sin embargo, agregaría una interfaz pintable.

También cambiaría algunos nombres. No es necesario crear confusión:

// common

public interface IComposite {
    public void addChild(Composite e);
}

public interface IViewee extends IComposite{
    public void validate();
    public List<IBound> getAbsoluteBouns();
}

public interface IVisual {
    public List<IBound> getBounds();
}

public interface IRec {
}

public interface IPaintable {
    public void paint();
}

// canvas

public interface ICanvasViewee extends IViewee, IPaintable {
}

public interface ICanvasVisual extends IViewee, IVisual {
}

public interface ICanvasRect extends ICanvasVisual, IRec {
}


// SVG

public interface ISVGViewee extends IViewee {
    public void element();
}

public interface ISVGVisual extends IVisual, ISVGViewee {
}

public interface ISVGRect extends ISVGVisual, IRect {
}
Tulains Córdova
fuente
Creo que las interfaces pueden ayudar en este caso. Me gustaría saber las razones por las cuales su respuesta fue rechazada.
umlcat
no el downvoter, pero las interfaces exponenciales de la
OMI
@arnaud, ¿qué quieres decir con "interfaces exponenciales"?
Tulains Córdova
@ user61852 ... bueno, digamos que son muchas interfaces. "exponencial" en realidad era un término incorrecto, es más como "multiplicativo". En el sentido de que si tuviera más "facetas" (compuesto, visual, pintable ...) y más "elementos" (lienzo, svg ...), terminaría con muchas interfaces.
Dagnelies
@arnaud Tienes un punto, pero al menos hay un diseño flexible de antemano y la pesadilla de herencia de OP se resolvería cuando no te sientas obligado a extender. Extiende alguna clase si lo desea y no se ve forzado por una jerarquía artificial.
Tulains Córdova
3

Esta es una pregunta general de OOP, suponiendo que el polimorfismo, los genéricos y los mixins estén disponibles. El lenguaje real que se utilizará es OOP Javascript (Typecript), pero es el mismo problema en Java o C ++.

Eso en realidad no es cierto en absoluto. El mecanografiado tiene una ventaja sustancial sobre Java, es decir, la tipificación estructural. Puede hacer algo similar en C ++ con plantillas de tipo pato, pero es mucho más esfuerzo.

Básicamente, defina sus clases, pero no se moleste en extender o definir ninguna interfaz. Luego, simplemente defina la interfaz que necesita y tómela como parámetro. Luego, los objetos pueden coincidir con esa interfaz; no necesitan saber para extenderla de antemano. Cada función puede declarar exactamente y solo los bits que le importan, y el compilador le dará un pase si el tipo final lo cumple, a pesar de que las clases en realidad no extienden esas interfaces explícitamente.

Esto lo libera de la necesidad de definir realmente una jerarquía de interfaz y definir qué clases deberían extender qué interfaces.

Simplemente defina cada clase y olvídese de las interfaces: la tipificación estructural se encargará de ello.

Por ejemplo:

class SVGViewee {
    validate() { /* stuff */ }
    addChild(svg: SVG) { /* stuff */ }
}
class CanvasViewee {
    validate() { /* stuff */ }
    paint() { /* stuff */ }
}
interface SVG {
    addChild: { (svg: SVG): void };
}
f(viewee: { validate: { (): boolean }; }) {
    viewee.validate();
}
g(svg: SVG) {
    svg.addChild(svg);
}
h(canvas: { paint: { (): void }; }) {
    canvas.paint();
}
f(SVGViewee());
f(CanvasViewee());
g(SVGViewee());
h(CanvasViewee());

Esto es mecanografiado totalmente legítimo. Tenga en cuenta que las funciones de consumo no saben ni dan una sola mierda sobre las clases base o interfaces utilizadas en la definición de las clases.

No importa si las clases están relacionadas o no por herencia. No importa si extendieron su interfaz. Simplemente defina la interfaz como el parámetro, y ya está; todas las clases que lo cumplan serán aceptadas.

DeadMG
fuente
Suena prometedor, pero realmente no entiendo la propuesta (lo siento, posiblemente demasiado sesgo OOP). ¿Quizás puedas compartir algún código de ejemplo? Por ejemplo, tanto svg.Viewee como canvas.Viewee necesitan un validate()método (cuya implementación es idéntica para ambos); entonces solo svg.Viewee necesita anular addChild () , mientras que solo canvas.Viewee necesita paint () (que llama a paint () en todos los elementos secundarios , que son miembros de la clase Composite base ). Así que realmente no puedo visualizar esto con la tipificación estructural.
Izhaki
Estás considerando un montón de cosas que simplemente no importan en este caso.
DeadMG
Así que probablemente no obtuve la respuesta en absoluto. Sería bueno si elaboras.
Izhaki
Hice una edición. La conclusión es que las clases base son absolutamente irrelevantes y a nadie le importan. Son solo un detalle de implementación.
DeadMG
1
OKAY. Esto está empezando a tener sentido. A) Sé lo que es tipear patos. B) No estoy seguro de por qué las interfaces son tan importantes en esta respuesta: el desglose de clases es por el hecho de compartir un comportamiento común, puede ignorar las interfaces de forma segura por ahora. C) Diga en su ejemplo que tiene SVGViewee.addChild(), pero CanvasVieweetambién necesita exactamente la misma función. Entonces, ¿parece tener sentido para mí que ambos sean inherentes a Composite?
Izhaki
3

vista rápida

Solución 3: El patrón de diseño de software "Jerarquía de clases paralelas" es tu amigo.

Respuesta extendida larga

Su diseño COMENZÓ A LA DERECHA. Puede optimizarse, algunas clases o miembros pueden eliminarse, pero la idea de "jerarquía paralela" que está aplicando para resolver un problema ES CORRECTA.

Tratar con el mismo concepto varias veces, generalmente en jerarquías de control.

Después de un tiempo, TERMINÉ HACIENDO LA MISMA SOLUCIÓN QUE OTROS DESARROLLADORES, que a veces se denomina Patrón de diseño de "Jerarquía paralela" o Patrón de diseño de "Jerarquía dual".

(1) ¿Alguna vez ha dividido una sola clase en una sola jerarquía de clases?

(2) ¿Alguna vez ha dividido una sola clase en varias clases, sin una jerarquía?

Si ha aplicado estas soluciones anteriores, por separado, son una forma de resolver algunos problemas.

Pero, ¿qué pasa si combinamos estas dos soluciones, simultáneamente?

Combínalos y obtendrás este "Patrón de diseño".

Implementación

Ahora, apliquemos el patrón de diseño de software "Jerarquía de clases paralelas" a su caso.

Actualmente tiene 2 o más jerarquías de clases independientes, que son muy similares, tienen asociaciones o propósitos similares, tienen propiedades o métodos similares.

Desea evitar tener código o miembros duplicados ("coherencia"), sin embargo, no puede fusionar estas clases directamente en una sola, debido a las diferencias entre ellas.

Entonces, sus jerarquías son muy similares a esta figura, pero, sin embargo, hay más de una:

................................................
...............+----------------+...............
...............|     Common::   |...............
...............|    Composite   |...............
...............+----------------+...............
...............|      ...       |...............
...............+-------+--------+...............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
...............+-------+--------+...............
...............|     Common::   |...............
...............|     Viewee     |...............
...............+----------------+...............
...............|      ...       |...............
...............+-------+--------+...............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Common::   |........|     Common::   |..
..|     Visual     |........|   Structural   |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 1

En este, aún no certificado, el Patrón de diseño, VARIAS JERARQUÍAS SIMILARES, SE FUSIONAN, EN UNA SOLA JERARQUÍA, y cada clase compartida o común se extiende por subclases.

Tenga en cuenta que esta solución es compleja, porque ya está lidiando con varias jerarquías, por lo tanto, es un escenario complejo.

1 La clase raíz

En cada jerarquía hay una clase "raíz" compartida.

En su caso, hay una clase "Compuesta" independiente, para cada jerarquía, que puede tener algunas propiedades similares y algunos métodos similares.

Algunos de esos miembros pueden fusionarse, algunos de esos miembros no pueden fusionarse.

Entonces, lo que puede hacer un desarrollador es crear una clase raíz base y subclasificar el caso equivalente para cada jerarquía.

En la Figura 2, puede ver un diagrama solo para esta clase, en el que cada clase mantiene su espacio de nombres.

Los miembros ya están omitidos.

................................................
...............+-------+--------+...............
...............|     Common::   |...............
...............|    Composite   |...............
...............+----------------+...............
...............|      ...       |...............
...............+-------+--------+...............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Canvas::   |........|      SVG::     |..
..|    Composite   |........|    Composite   |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 2

Como puede observar, cada clase "Compuesta" ya no se encuentra en una jerarquía separada, sino que se fusiona en una sola jerarquía compartida o común.

Luego, agreguemos los miembros, los que son iguales, se pueden mover a la superclase, y los que son diferentes, a cada clase base.

Y como ya sabe, los métodos "virtuales" o "sobrecargados" se definen en la clase base, pero se reemplazan en las subclases. Como la figura 3.

................................................
.............+--------------------+.............
.............|       Common::     |.............
.............|      Composite     |.............
.............+--------------------+.............
.............| [+] void AddChild()|.............
.............+---------+----------+.............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Canvas::   |........|      SVG::     |..
..|    Composite   |........|    Composite   |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 3

Tenga en cuenta que tal vez hay algunas clases sin miembros y que puede verse tentado a eliminar esas clases, NO. Se llaman "clases huecas", "clases enumerativas" y otros nombres.

2 Las subclases

Volvamos al primer diagrama. Cada clase "Compuesta" tenía una subclase "Viewee" en cada jerarquía.

El proceso se repite para cada clase. Tenga en cuenta que en la Figura 4, la clase "Common :: Viewee" desciende de "Common :: Composite", pero, por simplicidad, la clase "Common :: Composite" se omite del diagrama.

................................................
.............+--------------------+.............
.............|       Common::     |.............
.............|       Viewee       |.............
.............+--------------------+.............
.............|        ...         |.............
.............+---------+----------+.............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Canvas::   |........|      SVG::     |..
..|     Viewee     |........|     Viewee     |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 4

Notarás que "Canvas :: Viewee" y "SVG :: Viewee", NO YA NO descienden de su respectivo "Composite", sino del común "Common :: Viewee".

Puede agregar los miembros, ahora.

......................................................
.........+------------------------------+.............
.........|            Common::          |.............
.........|            Viewee            |.............
.........+------------------------------+.............
.........| [+] bool Validate()          |.............
.........| [+] Rect GetAbsoluteBounds() |.............
.........+-------------+----------------+.............
.......................|..............................
.......................^..............................
....................../.\.............................
.....................+-+-+............................
.......................|..............................
..........+------------+----------------+.............
..........|.............................|.............
..+-------+---------+........+----------+----------+..
..|      Canvas::   |........|         SVG::       |..
..|      Viewee     |........|        Viewee       |..
..+-----------------+........+---------------------+..
..|                 |........| [+] Viewee Element  |..
..+-----------------+........+---------------------+..
..| [+] void Paint()|........| [+] void addChild() |..
..+-----------------+........+---------------------+..
......................................................

Figure 5

3 Repita el proceso

El proceso continuará, para cada clase, "Canvas :: Visual" no descenderá de "Canvas :: Viewee", pero desde "Commons :: Visual", "Canvas :: Structural" no descenderá de "Canvas :: Viewee ", pero desde" Commons :: Structural ", y así sucesivamente.

4 El diagrama de jerarquía 3D

Terminará de obtener una especie de diagrama 3D, con varias capas, la capa superior, tiene la jerarquía "Común" y las capas inferiores, tiene cada jerarquía adicional.

Sus jerarquías de clases independientes originales, donde algo similar a esto (Figura 6):

.................................................
..+-----------------+.......+-----------------+..
..|      Common::   |.......|       SVG::     |..
..|     Composite   |.......|     Composite   |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+--------+--------+..
...........|.........................|...........
...........^.........................^...........
........../.\......................./.\..........
.........+-+-+.....................+-+-+.........
...........|.........................|...........
..+--------+--------+.......+--------+--------+..
..|      Common::   |.......|       SVG::     |..
..|      Viewee     |.......|      Viewee     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+--------+--------+..
...........|.........................|...........
...........^.........................^...........
........../.\......................./.\..........
.........+-+-+.....................+-+-+.........
...........|.........................|...........
..+--------+--------+.......+--------+--------+..
..|      Common::   |.......|       SVG::     |..
..|      Visual     |.......|      Visual     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+--------+--------+..
...........|.........................|...........
...........^.........................^...........
........../.\......................./.\..........
.........+-+-+.....................+-+-+.........
...........|.........................|...........
..+--------+--------+.......+--------+--------+..
..|      Common::   |.......|       SVG::     |..
..|       Rect      |.......|       Rect      |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+-----------------+.......+-----------------+..
.................................................

Figure 6

Tenga en cuenta que algunas clases se omiten, y toda la jerarquía "Canvas" se omite, por simplicidad.

La jerarquía de clase integrada final puede ser algo similar a esto:

.................................................
..+-----------------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|     Composite   |...\+..|     Composite   |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+-----------------+..
...........|.....................................
...........^.....................................
........../.\....................................
.........+-+-+...................................
...........|.....................................
..+--------+--------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|      Viewee     |...\+..|      Viewee     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+-----------------+..
...........|.....................................
...........^.....................................
........../.\....................................
.........+-+-+...................................
...........|.....................................
..+--------+--------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|      Visual     |...\+..|      Visual     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+-----------------+..
...........|.....................................
...........^.....................................
........../.\....................................
.........+-+-+...................................
...........|.....................................
..+--------+--------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|       Rect      |...\+..|       Rect      |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+-----------------+.......+-----------------+..
.................................................
Figure 7

Tenga en cuenta que algunas clases se omiten, y todas las clases "Canvas" se omiten, por simplicidad, pero serán similares a las clases "SVG".

Las clases "comunes" podrían representarse como una sola capa de un diagrama 3D, las clases "SVG" en otra capa y las clases "lienzo", en una tercera capa.

Verifique que cada capa esté relacionada con la primera, en la que cada clase tiene una clase padre de la jerarquía "Común".

La implementación del código puede requerir el uso de herencia de interfaz, herencia de clase o "mixins", dependiendo de lo que admita su lenguaje de programación.

Resumen

Como cualquier solución de programación, no se apresure a la optimización, la optimización es muy importante, sin embargo, una mala optimización puede convertirse en un problema mayor que el problema original.

No recomiendo aplicar "Solución 1" o "Solución 2".

En "Solución 1" no se aplica, porque, la herencia, se requiere en cada caso.

"Solución 2", "Mixins" pueden aplicarse, pero, después de diseñar las clases y las jerarquías.

Los mixins son una alternativa para la herencia basada en interfaz o la herencia múltiple basada en clases.

Mi propuesta, Solución 3, se llama a veces Patrón de diseño de "Jerarquía paralela" o Patrón de diseño de "Jerarquía dual".

Muchos desarrolladores / diseñadores no estarán de acuerdo y creen que no debería existir. Pero lo he utilizado yo mismo y otros desarrolladores como una solución común para problemas, como el de su pregunta.

Otra cosa que falta. En sus soluciones anteriores, el problema principal no era usar "mixins" o "interfaces", sino, para refinar, primero, el modelo de sus clases y luego usar una función de lenguaje de programación existente.

umlcat
fuente
Gracias por la respuesta muy completa. Creo que lo entendí bien, así que permítanme preguntar esto: canvas.vieweey todos sus descendientes necesitan un método llamado paint(). Ni el commonni las svgclases lo necesitan. Pero en su solución, la jerarquía está dentro common, no canvaso svgcomo en mi solución 2. Entonces, ¿cómo paint()termina exactamente en todas las subclases de canvas.vieweesi no hay herencia allí?
Izhaki
@Izhaki Lo siento si hay algunos errores en mi respuesta. Luego se paint()debe mover o declarar en "canvas :: viewee". La idea general del patrón se mantiene, pero se puede requerir que algunos miembros se muevan o cambien.
umlcat el
Bien, entonces, ¿cómo lo obtienen las subclases, si ninguna deriva de canvas::viewee?
Izhaki
¿Utilizaste una herramienta para crear tu arte ascii? (No estoy seguro de que los puntos realmente ayuden, por lo que vale.)
Aaron Hall
1

En un artículo titulado Patrones de diseño para tratar las jerarquías de herencia dual en C ++ , el tío Bob presenta una solución llamada Escalera al cielo . Es la intención declarada:

Este patrón describe la red de relaciones de herencia que se necesita cuando una jerarquía dada debe adaptarse, en su totalidad, a otra clase.

Y el diagrama proporcionado:

Un diagrama de clases con dos estructuras de herencia paralelas, donde cada clase a la derecha también es prácticamente inherente a su clase gemela a la izquierda.  La jerarquía izquierda también se basa completamente en la herencia virtual.

Aunque en la solución 2 no hay herencia virtual, está muy de acuerdo con el patrón Stairway to Heaven . Por lo tanto, la solución 2 parece razonable para este problema.

Izhaki
fuente