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;
// ...
}
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é umaForma, 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)