Design de software

Composição (Composition) vs. herança (Inheritance): 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).”

4 min de leitura
Imagem de capa de Composição (Composition) vs. herança (Inheritance): o verdadeiro significado de “não use herança”

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.

A herança traz tudo; a composição traz apenas o necessário
A herança traz tudo; a composição traz apenas o necessário

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.

  1. É uma relação is-a verdadeira: a filha é sempre um tipo do pai?
  2. Respeita o princípio da substituição de Liskov: é possível colocar a filha no lugar do pai sem nenhum problema?
  3. 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.

Quando colocamos em uma tabela, fica claro de relance o que usar e quando
Quando colocamos em uma tabela, fica claro de relance o que usar e quando

É 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.

Poder trocar como quem substitui blocos: esse é o charme da composição
Poder trocar como quem substitui blocos: esse é o charme da composição

É 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