As “hierarquias profundas da composição” também não são ruins?

19

Desculpas se "Hierarquia da composição" não é uma coisa, mas vou explicar o que quero dizer com isso na pergunta.

Não há programador de OO que não tenha encontrado uma variação de "Manter hierarquias de herança planas" ou "Preferir composição sobre herança" e assim por diante. No entanto, hierarquias profundas de composição também parecem problemáticas.

Digamos que precisamos de uma coleção de relatórios detalhando os resultados de uma experiência:

class Model {
    // ... interface

    Array<Result> m_results;
}

Cada resultado tem certas propriedades. Isso inclui o tempo do experimento, bem como alguns metadados de cada estágio do experimento:

enum Stage {
    Pre = 1,
    Post
};

class Result {
    // ... interface

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

OK ótimo. Agora, cada módulo do experimento possui uma sequência que descreve o resultado do experimento, bem como uma coleção de referências a conjuntos de amostras experimentais:

class ExperimentalModules {
    // ... interface

    String m_reportText;
    Array<Sample> m_entities;
}

E então cada amostra tem ... bem, você entendeu.

O problema é que, se estou modelando objetos do meu domínio de aplicativo, isso parece um ajuste muito natural, mas no final do dia, a Resulté apenas um contêiner estúpido de dados! Não parece valioso criar um grande grupo de classes para ele.

Supondo que as estruturas e classes de dados mostradas acima modelem corretamente os relacionamentos no domínio do aplicativo, existe uma maneira melhor de modelar esse "resultado", sem recorrer a uma hierarquia profunda de composição? Existe algum contexto externo que o ajude a determinar se esse design é bom ou não?

AwesomeSauce
fonte
Não entenda a parte do contexto externo.
Tulains Córdova
@ TulainsCórdova o que estou tentando perguntar aqui é "existem situações em que a composição profunda é uma boa solução"?
AwesomeSauce
Eu adicionei uma resposta.
Tulains Córdova 04/11
5
Sim, exatamente. Os problemas que as pessoas tentam resolver substituindo herança por composição não desaparecem magicamente apenas porque você mudou uma estratégia de agregação de recursos para outra. Ambas são soluções para gerenciar a complexidade, e essa complexidade ainda existe e precisa ser tratada de alguma maneira.
Mason Wheeler
3
Não vejo nada de errado nos modelos de dados profundos. Mas também não vejo como você poderia modelar isso com herança, já que é um relacionamento 1: N em cada camada.
usr

Respostas:

17

É disso que trata a Lei de Deméter / princípio do menor conhecimento . Um usuário da interface Modelnão precisa necessariamente saber sobre ExperimentalModulesa interface para realizar seu trabalho.

O problema geral é que, quando uma classe herda de outro tipo ou tem um campo público com algum tipo ou quando um método toma um tipo como parâmetro ou retorna um tipo, as interfaces de todos esses tipos se tornam parte da interface efetiva da minha classe. A questão é: esses tipos dos quais eu dependo: usar esses é o ponto principal da minha classe? Ou são apenas detalhes de implementação que eu expus acidentalmente? Porque se eles são detalhes de implementação, apenas os expus e permiti que os usuários da minha classe dependessem deles - meus clientes agora estão fortemente acoplados aos meus detalhes de implementação.

Portanto, se eu quiser melhorar a coesão, posso limitar esse acoplamento escrevendo mais métodos que fazem coisas úteis, em vez de exigir que os usuários acessem minhas estruturas privadas. Aqui, podemos oferecer métodos para procurar ou filtrar resultados. Isso facilita a refatoração da minha turma, já que agora posso alterar a representação interna sem alterar a minha interface.

Mas isso não tem um custo - a interface da minha classe exposta agora fica inchada com operações que nem sempre são necessárias. Isso viola o Princípio de Segregação de Interface: Não devo forçar os usuários a depender de mais operações do que realmente usam. E é aí que o design é importante: design é decidir qual princípio é mais importante no seu contexto e encontrar um compromisso adequado.

Ao projetar uma biblioteca que deve manter a compatibilidade de fontes em várias versões, eu estaria mais inclinado a seguir o princípio de menos conhecimento com mais cuidado. Se minha biblioteca usa outra biblioteca internamente, nunca exporia nada definido por ela a meus usuários. Se eu precisar expor uma interface da biblioteca interna, criaria um objeto wrapper simples. Padrões de design como o Bridge Bridge ou o Facade Pattern podem ajudar aqui.

Mas se minhas interfaces forem usadas apenas internamente ou se as interfaces que eu expor forem muito fundamentais (parte de uma biblioteca padrão ou de uma biblioteca tão fundamental que nunca faria sentido alterá-la), seria inútil ocultar essas interfaces. Como de costume, não há respostas únicas.

amon
fonte
Você mencionou uma grande preocupação que eu estava tendo; a ideia de que eu tinha que me aprofundar na implementação para fazer algo útil em níveis mais altos de abstração. Uma discussão muito útil, obrigado.
precisa
7

As “hierarquias profundas da composição” também não são ruins?

Claro. A regra deve realmente dizer "mantenha todas as hierarquias o mais niveladas possível".

Mas as hierarquias profundas da composição são menos frágeis do que as hierarquias profundas da herança e mais flexíveis para mudar. Intuitivamente, isso não deve surpreender; hierarquias familiares são da mesma maneira. Seu relacionamento com seus parentes de sangue é fixo em genética; seus relacionamentos com seus amigos e sua esposa não são.

Seu exemplo não me parece uma "hierarquia profunda". Uma forma herda de um retângulo que herda de um conjunto de pontos que herda de coordenadas x e y, e eu ainda nem consegui uma cabeça cheia de vapor ainda.

Robert Harvey
fonte
4

Parafraseando Einstein:

mantenha todas as hierarquias o mais niveladas possível, mas não mais horizontalmente

Ou seja, se o problema que você está modelando é assim, você não deve ter escrúpulos em modelá-lo como ele é.

Se o aplicativo fosse apenas uma planilha gigante, não será necessário modelar a hierarquia de composição. Caso contrário, faça. Não acho que a coisa seja infinitamente profunda. Apenas mantenha os getters que retornam coleções preguiçosos se o preenchimento dessas coleções for caro, como solicitá-las para um repositório que as puxe para formar um banco de dados. Por preguiçoso, quero dizer, não os preencha até que sejam necessários.

Tulains Córdova
fonte
1

Sim, você pode modelá-lo como tabelas relacionais

Result {
    Id
    ModelId
    Epoch
}

etc., que muitas vezes podem levar a uma programação mais fácil e mapeamento simples para um banco de dados. mas à custa de perder a codificação codificada de seus relacionamentos

Ewan
fonte
0

Eu realmente não sei como resolver isso em Java, porque o Java é construído em torno da ideia de que tudo é um objeto. Você não pode suprimir classes intermediárias substituindo-as por um dictionnary porque os tipos nos seus blobs de dados são heterogêneos (como um registro de data e hora + lista ou seqüência de caracteres + lista ...). A alternativa de substituir uma classe de contêiner por seu campo (por exemplo, passar uma variável "m_epoch" por um "m_modules") é simplesmente ruim, porque está criando um objeto implícito (você não pode ter um sem o outro) sem nenhum detalhe específico. benefício.

Na minha linguagem de predileção (Python), minha regra geral seria não criar um objeto se nenhuma lógica específica (exceto a obtenção e configuração básicas) pertencer a ele. Mas, dadas as suas restrições, acho que a hierarquia de composição que você possui é o melhor design possível.

Arthur Havlicek
fonte