Composição vs. herança: o verdadeiro significado de “não use herança”
Ao estudar programação orientada a objetos, há uma frase que você inevitavelmente encontra: “Prefira composição à herança (Favor composition over inheritance).”
A conclusão é que essa frase não significa “nunca use herança”, mas sim “não abuse da herança para reutilizar código”. A herança continua sendo uma ferramenta válida, mas, na maioria das situações que exigem reutilização, a composição é uma escolha mais segura.
Neste artigo, vou explicar, com base na minha experiência, como as duas opções se diferenciam no código real e quando escolher cada uma.
Qual é a diferença entre herança e composição?
Vamos começar com uma divisão bem simples.
Herança é uma relação “é um (is-a)”. Um cachorro é um animal, por exemplo. A classe filha herda diretamente as funcionalidades da classe pai.
Composição é uma relação “tem um (has-a)”, como um carro tem um motor. Você mantém dentro dele um objeto com a funcionalidade necessária e delega o trabalho a esse objeto.
Com código, fica mais fácil entender. Abaixo está a forma de herdar funcionalidades por herança.
// Herança: Stackrecebe NSMutableArraytodos os métodos
class Stack: NSMutableArray {
func push(_ o: Any) { add(o) }
func pop() -> Any {
let o = lastObject!
removeLastObject()
return o
}
}
O problema é que, desse jeito, o Stack também expõe externamente métodos indesejados, como insert(_:at:) e removeObject(at:).
Abaixo está a mesma implementação transformada usando composição.
// Composição: selecionar apenas as funcionalidades necessárias e delegá-las
class Stack {
private var list: [Any] = []
func push(_ o: Any) { list.append(o) }
func pop() -> Any { list.removeLast() }
}
Mantemos o array dentro e expomos apenas as operações necessárias. O Stack fica apenas com o que realmente precisa fazer.
O verdadeiro significado de “não use herança”
Essa recomendação surgiu por causa de dois pontos fracos representativos da herança.
Primeiro, o encapsulamento é quebrado. A classe filha passa a depender da implementação interna da classe pai. Quando o código do pai muda, a filha pode quebrar de repente, mesmo tendo funcionado perfeitamente antes.
Segundo, o acoplamento fica alto demais. Pai e filha ficam fortemente ligados em tempo de compilação, o que dificulta mudar essa relação depois.
Herança não é uma ferramenta para reutilizar código, mas uma ferramenta para definir tipos.
Essa frase é o ponto central. No momento em que você usa herança apenas porque quer reutilizar código, a relação começa a se complicar.
O quadrado e o retângulo são um exemplo famoso. Matematicamente, um quadrado é um retângulo, então parece que a herança seria adequada. Porém, ao herdar a capacidade do retângulo de “alterar largura e altura separadamente”, o quadrado deixa de ser um quadrado.
Mesmo que pareça uma relação is-a, se você não puder substituir completamente também o comportamento, a herança vira uma armadilha.
Então, quando é aceitável usar herança?
Herança não é sempre ruim. Se todas as condições abaixo forem atendidas, ela pode ser até mais simples.
- É uma relação is-a verdadeira: a filha é sempre um tipo do pai?
- Respeita o princípio da substituição de Liskov: é possível colocar a filha no lugar do pai sem nenhum problema?
- O pai foi projetado pensando em herança: ele está documentado e aberto para extensão?
Se as três respostas forem positivas, tudo bem usar herança. Estender classes baseadas em frameworks, como UIViewController, é um caso típico.
Por outro lado, se uma delas for duvidosa, pense primeiro em composição.
Fiz uma comparação simples em uma tabela.
| Situação | Recomendação |
|---|---|
| Relação is-a pura, com substituição completa | Herança |
| Quando você quer apenas reutilizar código | Composição |
| Quando quer mudar o comportamento em tempo de execução | Composição |
| Quando precisa combinar várias funcionalidades | Composição |
Como você pode ver, a maioria das situações práticas tende para a composição. O conselho “favor composition” não surgiu por acaso.
É assim que eu decido no trabalho
Ao criar uma classe nova, costumo perguntar primeiro: “Isto é um tipo do pai ou quero apenas pegar emprestada a funcionalidade dele?”
Se quero apenas pegá-la emprestada, quase sempre escolho composição.
Os padrões de projeto também deixam a direção clara. Padrões como Strategy e Decorator são todos baseados em composição.
Especialmente o padrão Strategy, que separa o comportamento em um objeto e o troca em tempo de execução. É uma flexibilidade difícil de imitar com herança.
É claro que você precisa aceitar que o código fique um pouco mais longo, porque terá de escrever os métodos de delegação manualmente. Ainda assim, depois fica muito mais fácil mudar a estrutura.
“Não use herança” não significa proibi-la, mas alertar contra seu abuso para fins de reutilização. Se o is-a for claro, use herança; se você só quiser pegar uma funcionalidade emprestada, use composição. Só esse critério já deixa o código muito mais sólido. Espero que você faça bons projetos hoje também!
Leitura recomendada
- Abstração (Abstraction) e encapsulamento (Encapsulation) em OOP: se você sempre se confunde, este artigo resolve
- O verdadeiro motivo para usar a palavra-chave final do Swift em classes (resumo completo de desempenho e design)
- A essência da orientação a objetos são as mensagens: o verdadeiro OOP de Alan Kay

