Hierarquias paralelas - parcialmente iguais, parcialmente diferentes

12

Existem algumas perguntas semelhantes por aí 1 ,2 ,3 ,4 , mas não parece exatamente o caso nesta questão, nem as soluções parecem ótimas.

Esta é uma questão geral de POO, assumindo que polimorfismo, genéricos e mixins estão disponíveis. A linguagem real a ser usada é o Javascript OOP (TypeScript), mas é o mesmo problema em Java ou C ++.

Eu tenho hierarquias de classes paralelas, que às vezes compartilham o mesmo comportamento (interface e implementação), mas às vezes cada uma tem seu próprio comportamento "protegido". Ilustrado assim:

3 hierarquias de classes paralelas, a coluna central mostra as partes comuns, a coluna esquerda é a hierarquia da tela e a coluna direita mostra a hierarquia SVG

Isso é apenas para fins ilustrativos ; não é o diagrama de classes real. Para ler:

  • Qualquer coisa na hierarquia comum (centro) é compartilhada entre as hierarquias Canvas (esquerda) e SVG (direita). Por compartilhamento, quero dizer interface e implementação.
  • Qualquer coisa apenas nas colunas esquerda ou direita significa um comportamento (métodos e membros) específico para essa hierarquia. Por exemplo:
    • As hierarquias esquerda e direita usam exatamente os mesmos mecanismos de validação, mostrados como um único método ( Viewee.validate()) na hierarquia comum.
    • Somente a hierarquia da tela tem um método paint(). Este método chama o método de pintura em todas as crianças.
    • A hierarquia SVG precisa substituir o addChild()método de Composite, mas esse não é o caso da hierarquia da tela.
  • As construções das duas hierarquias laterais não podem ser misturadas. Uma fábrica garante isso.

Solução I - Tease Apart Herança

O Tease Apart Inheritance de Fowler não parece fazer o trabalho aqui, porque há alguma discrepância entre os dois paralelos.

Solução II - Mixins

Este é o único que eu posso pensar atualmente. As duas hierarquias são desenvolvidas separadamente, mas em cada nível as classes misturam a classe comum, que não fazem parte de uma hierarquia de classes. Omitindo o structuralgarfo, ficará assim:

As três colunas novamente, as colunas esquerda e direita são hierarquias paralelas, onde cada classe também é inerente a uma classe comum.  As classes comuns não fazem parte de uma hierarquia

Observe que cada coluna estará em seu próprio espaço para nome, para que os nomes das classes não entrem em conflito.

A questão

Alguém pode ver falhas nessa abordagem? Alguém pode pensar em uma solução melhor?


Termo aditivo

Aqui está um código de exemplo de como isso deve ser usado. O espaço para nome svgpode ser substituído por 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 );

Essencialmente, em tempo de execução, os clientes compõem a hierarquia de instâncias das subclasses Viewee; igual a:

Uma imagem mostrando uma hierarquia de objetos como layer, rect, scroller, etc.

Digamos que todos esses espectadores sejam da hierarquia da tela, eles são renderizados atravessando a hierarquia que pode chamar paint()cada espectador. Se eles pertencem à hierarquia svg, os espectadores sabem como se adicionar ao DOM, mas não há paint()travessia.

Izhaki
fonte
Leitura recomendada: Design Review: no tópico ou não?
Robert Harvey
Talvez tente o padrão de design completo do Decorator (Erich Gamma et als, Design Patterns)?
Zon
O que é um espectador? O que "paralelo" significa como um substantivo (em oposição a um adjetivo)?
Tulains Córdova
Você tem herança múltipla?
Tulains Córdova
As classes Canvas ou SVG contêm estado ou dados adicionais que não estão em comum? Como você usa as aulas? Você pode mostrar algum código de exemplo mostrando como essas pesquisas podem ser usadas?
Euphoric

Respostas:

5

A segunda abordagem separa melhor as interfaces, seguindo o princípio de segregação de interfaces.

No entanto, eu adicionaria uma interface Paintable.

Eu também mudaria alguns nomes. Não há necessidade de criar confusão:

// 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
fonte
Penso que as interfaces podem ajudar neste caso. Gostaria de saber as razões pelas quais sua resposta foi rebaixada.
umlcat
não o downvoter, mas as interfaces IMHO exponencial não são um bom padrão
dagnelies
@arnaud o que você quer dizer com "interfaces exponenciais"?
Tulains Córdova
@ user61852 ... bem, digamos que há muitas interfaces. "exponencial" era na verdade um termo errado, é mais como "multiplicativo". No sentido de que se você tivesse mais "facetas" (compostas, visuais, pintáveis ​​...) e mais "elementos" (canvas, svg ...), você acabaria com muitas interfaces.
Dagnelies
@arnaud Você tem razão, mas pelo menos há um design flexível de antemão, e o pesadelo da herança do OP seria resolvido quando você não se sentir obrigado a se estender. Você estende alguma classe, se quiser e não forçado por uma hierarquia artificial.
Tulains Córdova
3

Esta é uma questão geral de POO, assumindo que polimorfismo, genéricos e mixins estão disponíveis. A linguagem real a ser usada é o Javascript OOP (TypeScript), mas é o mesmo problema em Java ou C ++.

Na verdade, isso não é verdade. O TypeScript possui uma vantagem substancial sobre o Java, a saber, tipagem estrutural. Você pode fazer algo semelhante em C ++ com modelos digitados por pato, mas é muito mais esforço.

Basicamente, defina suas classes, mas não se preocupe em estender ou definir nenhuma interface. Em seguida, basta definir a interface necessária e tomá-la como parâmetro. Em seguida, os objetos podem corresponder a essa interface - eles não precisam saber para estendê-la com antecedência. Cada função pode declarar exatamente e apenas os bits de que eles se importam, e o compilador fornecerá uma aprovação se o tipo final atender, mesmo que as classes não estendam explicitamente essas interfaces.

Isso libera a necessidade de realmente definir uma hierarquia de interface e definir quais classes devem estender quais interfaces.

Basta definir cada classe e esquecer as interfaces - a digitação estrutural cuidará disso.

Por exemplo:

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());

Isso é totalmente datilografado legítimo. Observe que as funções de consumo não conhecem ou dão uma única merda sobre as classes base ou interfaces usadas na definição das classes.

Não importa se as classes são relacionadas ou não por herança. Não importa se eles estenderam sua interface. Basta definir a interface como o parâmetro e pronto - todas as classes que a atendem são aceitas.

DeadMG
fonte
Parece promissor, mas eu realmente não entendo a proposta (desculpe, possivelmente muito viés de OOP). Talvez você possa compartilhar algum exemplo de código? Por exemplo, svg.Viewee e canvas.Viewee precisam de um validate()método (cuja implementação é idêntica para ambos); somente svg.Viewee precisa substituir addChild () , enquanto apenas canvas.Viewee precisa de paint () (que chama paint () em todos os filhos - que são membros da classe Composite base ). Então, eu realmente não consigo visualizar isso com a tipagem estrutural.
precisa saber é o seguinte
Você está considerando um monte de coisas que simplesmente não importam nesse caso.
precisa saber é
Então eu provavelmente não recebi a resposta. Seria bom se você elaborar.
Izhaki
Eu fiz uma edição. A linha inferior é que as classes base são absolutamente irrelevantes e ninguém se importa com elas. Eles são apenas um detalhe de implementação.
DeadMG 21/01
1
ESTÁ BEM. Isso está começando a fazer sentido. A) Eu sei o que é digitação de pato. B) Não sei por que as interfaces são tão centrais nesta resposta - a divisão de classe é para compartilhar o comportamento comum; você pode ignorar com segurança as interfaces por enquanto. C) Diga no seu exemplo que você tem SVGViewee.addChild(), mas CanvasVieweetambém precisa exatamente do mesmo recurso. Então, parece fazer sentido para mim que ambos inerentes ao Composite?
Izhaki
3

Visão geral rápida

Solução 3: O padrão de design de software "Hierarquia de classes paralelas" é seu amigo.

Resposta prolongada

Seu design COMEÇOU CERTO. Ele pode ser otimizado, algumas classes ou membros podem ser removidos, mas a idéia da "hierarquia paralela" que você está aplicando para resolver um problema É CERTA.

Lide com o mesmo conceito várias vezes, geralmente em hierarquias de controle.

Depois de um tempo, eu terminei de fazer a mesma solução que outros desenvolvedores, que às vezes é chamado de Padrão de design "Hierarquia paralela" ou Padrão de design "Hierarquia dupla".

(1) Você já dividiu uma única classe em uma única hierarquia de classes?

(2) Você já dividiu uma única classe em várias classes, sem hierarquia?

Se você aplicou essas soluções anteriores separadamente, elas são uma maneira de resolver alguns problemas.

Mas, e se combinarmos essas duas soluções simultaneamente?

Combine-os e você obterá esse "Padrão de Design".

Implementação

Agora, vamos aplicar o padrão de design de software "Parallel Class Hierarchy", ao seu caso.

Atualmente, você possui 2 ou mais hierarquias independentes de classes, que são muito semelhantes, têm associações ou propósitos semelhantes, têm propriedades ou métodos semelhantes.

Você deseja evitar o código ou membros duplicados ("consistência"), mas não pode mesclar essas classes diretamente em uma única, devido às diferenças entre elas.

Portanto, suas hierarquias são muito semelhantes a essa figura, mas, ainda assim, existem mais de uma:

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

Figure 1

Neste, ainda não certificado, Design Pattern, VÁRIAS HIERARQUIAS SIMILARES, SÃO FUSIONADAS EM UMA HIERARQUIA ÚNICA, e cada classe compartilhada ou comum é estendida por subclassificação.

Observe que essa solução é complexa, porque você já está lidando com várias hierarquias, portanto, é um cenário complexo.

1 A classe raiz

Em cada hierarquia, há uma classe "raiz" compartilhada.

No seu caso, existe uma classe "Composite" independente, para cada hierarquia, que pode ter algumas propriedades semelhantes e alguns métodos semelhantes.

Alguns desses membros podem ser mesclados, outros não podem ser mesclados.

Portanto, o que um desenvolvedor pode fazer é criar uma classe raiz básica e subclassificar o caso equivalente para cada hierarquia.

Na Figura 2, você pode ver um diagrama apenas para esta classe, na qual cada classe mantém seu espaço para nome.

Os membros já foram omitidos.

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

Figure 2

Como você pode observar, cada classe "Composta" não está mais em uma hierarquia separada, mas mesclada em uma única hierarquia compartilhada ou comum.

Então, vamos adicionar os membros, aqueles que são iguais, podem ser movidos para a superclasse e os que são diferentes para cada classe base.

E como você já sabe, os métodos "virtual" ou "sobrecarregado" são definidos na classe base, mas substituídos nas subclasses. Como na Figura 3.

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

Figure 3

Observe que talvez haja algumas classes sem membros e, você pode ser tentado a remover essas classes, NÃO. Eles são chamados de "Classes ocas", "Classes enumerativas" e outros nomes.

2 As subclasses

Vamos voltar ao primeiro diagrama. Cada classe "Composite" tinha uma subclasse "Viewee", em cada hierarquia.

O processo é repetido para cada classe. Observe que na Figura 4, a classe "Common :: Viewee" desce da "Common :: Composite", mas, por simplicidade, a classe "Common :: Composite" é omitida no diagrama.

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

Figure 4

Você notará que "Canvas :: Viewee" e "SVG :: Viewee" NÃO descem mais do respectivo "Composite", mas, do "Common :: Viewee" comum.

Você pode adicionar os membros agora.

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

Figure 5

3 Repita o processo

O processo continuará, para cada classe, "Canvas :: Visual" não descerá de "Canvas :: Viewee", comprará "Commons :: Visual", "Canvas :: Structural" não descerá de "Canvas :: Viewee ", compre em" Commons :: Structural "e assim por diante.

4 O diagrama de hierarquia 3D

Você terminará de obter uma espécie de diagrama 3D, com várias camadas, a camada superior, a hierarquia "Comum" e as camadas inferiores, cada hierarquia adicional.

Suas hierarquias de classe independentes originais, onde algo semelhante a este (Figura 6):

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

Figure 6

Observe que algumas classes são omitidas e toda a hierarquia "Canvas" é omitida, por simplicidade.

A hierarquia de classes integrada final pode ser algo semelhante a este:

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

Observe que algumas classes são omitidas e todas as classes "Canvas" são omitidas, por simplicidade, mas serão semelhantes às classes "SVG".

As classes "Comuns" podem ser representadas como uma única camada de um diagrama 3D, as classes "SVG" em outra camada e as classes "Canvas", em uma terceira camada.

Verifique se cada camada está relacionada à primeira, na qual cada classe possui uma classe pai da hierarquia "Comum".

A implementação do código pode exigir o uso de herança de interface, herança de classe ou "mixins", dependendo do que a linguagem de programação suportar.

Sumário

Como qualquer solução de programação, não se apresse na otimização, a otimização é muito importante, mas uma otimização ruim pode se tornar um problema maior do que o problema original.

Não recomendo aplicar "Solução 1" ou "Solução 2".

Na "Solução 1" não se aplica, porque a herança é necessária em cada caso.

"Solução 2", "Mixins" podem ser aplicadas, mas depois de projetar as classes e hierarquias.

Mixins, são uma alternativa para herança baseada em interface ou herança múltipla baseada em classe.

Minha proposta, Solução 3, às vezes é chamada de Padrão de Design "Hierarquia Paralela" ou Padrão de Design "Hierarquia Dupla".

Muitos desenvolvedores / designers não concordam com isso e acreditam que não deveria existir. Mas, eu usei sozinho e outros desenvolvedores como uma solução comum para problemas, como o da sua pergunta.

Outra coisa que falta. Nas soluções anteriores, o problema principal não era o de usar "mixins" ou "interfaces", mas refinar primeiro o modelo de suas classes e, posteriormente, usar um recurso de Linguagem de Programação existente.

umlcat
fonte
Obrigado pela resposta muito completa. Eu acho que entendi bem, então deixe-me perguntar isso: canvas.vieweee todos os seus descendentes precisam de um método chamado paint(). Nem as classes commonnem as svgprecisam. Mas na sua solução, a hierarquia está dentro common, não é canvasou é svgcomo na minha solução 2. Então, como exatamente paint()termina em todas as subclasses de canvas.vieweese não há herança lá?
Izhaki 21/01
@ Izhaki Desculpe se há alguns bugs na minha resposta. Em seguida, paint()deve ser movido ou declarado em "canvas :: viewee". A ideia geral do padrão permanece, mas alguns membros podem precisar ser movidos ou alterados.
umlcat
OK, então como as subclasses conseguem, se nenhuma deriva disso canvas::viewee?
precisa saber é o seguinte
Você usou uma ferramenta para criar sua arte ascii? (Eu não tenho certeza que os pontos realmente ajuda, por que vale a pena.)
Aaron Hall
1

Em um artigo intitulado Padrões de design para lidar com hierarquias de herança dupla em C ++ , o tio Bob apresenta uma solução chamada Stairway to Heaven . Sua intenção é declarada:

Esse padrão descreve a rede de relacionamentos de herança que é necessária quando uma determinada hierarquia deve ser adaptada, na sua totalidade, a outra classe.

E o diagrama forneceu:

Um diagrama de classes com duas estruturas de herança paralelas, em que cada classe à direita também é virtualmente inerente à sua classe gêmea à esquerda.  A hierarquia esquerda também se baseia completamente na herança virtual

Embora na solução 2 não haja herança virtual, ela está muito de acordo com o padrão Stairway to Heaven . Assim, a solução 2 parece razoável para esse problema.

Izhaki
fonte