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:
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 deComposite
, mas esse não é o caso da hierarquia da tela.
- As hierarquias esquerda e direita usam exatamente os mesmos mecanismos de validação, mostrados como um único método (
- 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 structural
garfo, ficará assim:
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 svg
pode 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:
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.
Respostas:
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:
fonte
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:
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.
fonte
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.SVGViewee.addChild()
, masCanvasViewee
também precisa exatamente do mesmo recurso. Então, parece fazer sentido para mim que ambos inerentes ao Composite?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:
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.
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.
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.
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.
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):
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:
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.
fonte
canvas.viewee
e todos os seus descendentes precisam de um método chamadopaint()
. Nem as classescommon
nem assvg
precisam. Mas na sua solução, a hierarquia está dentrocommon
, não écanvas
ou ésvg
como na minha solução 2. Então, como exatamentepaint()
termina em todas as subclasses decanvas.viewee
se não há herança lá?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.canvas::viewee
?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:
E o diagrama forneceu:
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.
fonte