DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

Livro Head First Design Pattern: Prefira composição em vez de herança

Introdução

Herança modela uma relação "é um" (is-a): MallardDuck é um Duck. Composição modela uma relação "tem um" (has-a): Duck tem um FlyBehavior.

O princípio diz: quando você precisa reutilizar código ou compor comportamento, prefira montar um objeto a partir de peças menores (composição) a herdar de uma superclasse cada vez mais inchada. Isso não significa banir herança — significa não usá-la como ferramenta padrão para todo tipo de reuso.

Por que herança vira um problema

Você herda comportamento, mas herda tudo, mesmo o que não serve. E como a subclasse é compilada contra a estrutura interna da superclasse, qualquer mudança na superclasse se propaga — às vezes de forma inesperada — para toda a árvore de descendentes. Isso é chamado de fragile base class problem (problema da classe-base frágil): você não consegue mais mexer na classe-mãe com segurança, porque não sabe quais das dezenas de subclasses dependem de algum detalhe interno dela.

O sintoma mais visível, porém, é a explosão combinatória: quando um comportamento varia em mais de uma dimensão, cada combinação nova de dimensões vira uma classe nova.

!image.png

Repare no padrão: são só 2 dimensões (voa ou não / faz som ou fica mudo), cada uma com 2 opções, e isso já gera 4 classes concretas. Some uma terceira dimensão — digamos, cor da plumagem, com 2 variações — e você pula para 8 classes. É crescimento multiplicativo, não aditivo: 2x2x2 = 8, não 2 + 2 + 2 = 6. Cada combinação nova de comportamentos que alguém pedir vira uma classe nova, e a maior parte do código dentro dela é cópia com pequenas variações.

A Correção

Em vez de Duck ramificar em subclasses por combinação de comportamento, Duck passa a compor dois objetos independentes — um FlyBehavior e um QuackBehavior — e delega a eles:

abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Com isso, 2 variações de voo + 2 variações de som não geram 4 classes — geram 2 + 2 = 4 objetos de comportamento, que são livremente combinados em qualquer Duck. Ao adicionar uma terceira dimensão, você não multiplica: apenas soma mais uma família de comportamentos independente. É o mesmo diagrama de composição que te mostrei antes, só que agora você já entende o problema que ele existe para evitar.

Quando herança ainda é a escolha certa

O princípio não é "nunca use herança" — é "não use herança como padrão automático". Ela continua sendo a ferramenta certa quando:

  • a relação é-um é genuinamente verdadeira e estável (um Círculo é uma Forma, e sempre será);
  • as subclasses respeitam o Liskov Substitution Principle — qualquer subclasse pode substituir a superclasse sem quebrar expectativas de quem usa;
  • a hierarquia é rasa (1-2 níveis) e não vai crescer em múltiplas dimensões independentes;
  • você quer reaproveitar uma implementação já testada e não espera variação de comportamento em runtime.

O sinal de alerta para trocar por composição é quando você percebe que está adicionando um método a uma subclasse só para sobrescrevê-lo como vazio ou lançar UnsupportedOperationException (RubberDuck.fly() { }) — isso é sintoma clássico de que a hierarquia está forçando um comportamento que não pertence a todos os descendentes.

Padrões que existem para viabilizar composição

Vários padrões do catálogo GoF são, na prática, formas estruturadas de aplicar este princípio:

  • Strategy — comportamento intercambiável via composição (FlyBehavior).
  • Decorator — adiciona responsabilidades a um objeto envolvendo-o em outro objeto com a mesma interface, em vez de criar uma subclasse para cada combinação de responsabilidades (ex: CoffeeWithMilk, CoffeeWithMilkAndSugar... vira apenas decoradores empilhados).
  • Composite — monta estruturas de árvore (parte-todo) compondo objetos recursivamente, sem hierarquia de herança rígida entre "folha" e "galho".
  • Bridge — separa uma abstração da sua implementação em duas hierarquias independentes que se compõem, exatamente para evitar a explosão combinatória que o diagrama acima mostra.

O problema do "diamante" (bônus em linguagens com herança múltipla)

Em linguagens que permitem herdar de mais de uma classe (C++, Python), a explosão de herança pode gerar um problema extra: se ClasseD herda de ClasseB e ClasseC, e ambas herdam de ClasseA, qual versão de um método sobrescrito em B e C prevalece em D? Isso é o diamond problem, e é uma das razões pelas quais Java e C# nunca permitiram herança múltipla de classes — só de interfaces (que não carregam implementação, então o conflito não existe). Composição simplesmente não sofre disso: você nunca tem ambiguidade sobre "de qual pai herdei esse método", porque não há herança nenhuma envolvida.

Resumo

Herança Composição
Relação modelada é-um tem-um
Acoplamento forte (subclasse depende da estrutura interna da superclasse) fraco (depende só da interface do componente)
Variação em N dimensões multiplicativa (classes explodem) aditiva (soma componentes)
Troca de comportamento fixada em tempo de compilação pode mudar em tempo de execução (setFlyBehavior)
Risco principal fragile base class problem mais indireção, mais peças para montar mentalmente
Quando usar relação é-um genuína, hierarquia rasa e estável comportamento varia, ou varia em mais de uma dimensão

Top comments (0)