DEV Community

Cover image for Composição vs. Herança: Por que a composição é a melhor opção na maioria dos casos?
João Gabriel de Souza
João Gabriel de Souza

Posted on

Composição vs. Herança: Por que a composição é a melhor opção na maioria dos casos?

A herança é um dos primeiros conceitos que aprendemos ao programar orientado a objetos. A ideia de criar classes derivadas que herdam funcionalidades de uma classe pai parece uma solução elegante para reutilizar código. No entanto, à medida que um projeto cresce e evolui, os problemas da herança começam a aparecer.

Neste artigo, discutiremos por que a composição é uma abordagem mais flexível e sustentável para reutilização de código, abordando suas vantagens e como utilizá-la corretamente.

Mas o que é Herança?

A herança permite que uma classe (subclasse) herde funcionalidades de outra classe (superclasse). Isso cria uma hierarquia onde a subclasse pode reutilizar ou modificar comportamentos definidos na superclasse.

A herança tem duas capacidades: a capacidade de reutilizar o código e a capacidade de construir uma abstração. Ela permite que um consumidor pense que está recebendo uma superclasse, mas na verdade está recebendo uma subclasse.

Os problemas com Herança

A herança pode parecer uma solução conveniente, mas impõe desafios significativos:

1. Acoplamento Excessivo

A herança cria um forte acoplamento entre a superclasse e suas subclasses. Mudanças na superclasse podem quebrar as subclasses, mesmo que estas não dependam diretamente das modificações.

2. Fragilidade do Código

Como as subclasses dependem da superclasse, alterações podem introduzir bugs inesperados e difíceis de rastrear.

3. Rigidez na Evolução do Código

Uma estrutura hierárquica pode se tornar inflexível com o tempo, dificultando alterações e extensões.

Os problemas da herança aparecem quando você precisa modificar o código. A mudança é o inimigo do design perfeito, e você muitas vezes se prende a uma estrutura difícil de alterar

Imaginando um exemplo

Vamos olhar um exemplo utilizando a linguagem de programação PHP:

class Veiculo {
     public function buzinar() {
        return "Beep! Beep!";
    }

    public function empinar() {
        return "Empinando!";
    }
}

class Carro extends Veiculo {
    public function buzinar() {
        return "Fon! Fon!";
    }

    public function empinar() {
        throw new Exception("Carro não empina!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui temos um exemplo de funcionalidade herdada pela superclasse no qual uma subclasse não precisa implementar. Ao longo da vida do software, basta que regras ou necessidades mudem para que problemas como esse se repitam ao longo de todo o código.

E o que é Composição?

A composição envolve a criação de objetos que contêm instâncias de outras classes, permitindo que elas trabalhem juntas sem a necessidade de uma relação hierárquica.

A composição reduz a superfície de contato entre objetos, o que resulta em menos atrito conforme surgem mudanças

Na composição, você não precisa agrupar todos os elementos comuns em uma superclasse e não precisa alterar nenhuma das outras classes para adicionar novas classes.

Com a composição, temos:

1. Maior Flexibilidade

A composição permite combinar classes de maneira mais modular e flexível, adaptando o código às necessidades específicas.

2. Menos Acoplamento

Os objetos são independentes e podem ser modificados ou substituídos sem impactar outras partes do sistema.

3. Reutilização Granular

Apenas as partes necessárias do código são reutilizadas, evitando herdar funcionalidades desnecessárias.

Exemplo de Composição

class Veiculo {
    public function buzinar() {
        return "Beep! Beep!";
    }

     public function empinar() {
        return "Empinando!";
    }
}

class Carro {
 {
    private $veiculo; 

    public function __construct(Veiculo $veiculo = null) {
        $this->veiculo = $veiculo;
    }

    public function buzinar() {
        return $this->veiculo->buzinar();
    }

}
Enter fullscreen mode Exit fullscreen mode

Dessa forma, apesar do método empinar() estar disponível, nós não precisamos nos preocupar com ele. E o melhor de tudo, se o método empinar() precisar ser modificado, ele não vai afetar a nossa classe Carro.

Interfaces

Você já deve ter ouvido falar de Interfaces, certo? As interfaces vão ter um papel fundamental na flexibilidade de compartilhamento de código.
As interfaces definem um contrato, especificando os métodos que uma classe deve implementar. Isso permite a criação de abstrações sem os problemas da herança.

Vamos ver como fica o nosso exemplo adicionando interfaces:

interface VeiculoInterface {
    public function buzinar();
}

class Carro implements VeiculoInterface {
    public function buzinar() {
        return "Fon! Fon!";
    }
}

class Moto implements VeiculoInterface {
    public function buzinar() {
        return "Bip! Bip!";
    }
}
Enter fullscreen mode Exit fullscreen mode

Veja que, agora, as classes que assinam o contrato com a interface precisam implementar os métodos daquela interface. Toda classe que implementar VeiculoInterface pode ser utilizada sem precisar herdar funcionalidades desnecessárias.

A utilização ficaria mais ou menos assim:

public class testarVeiculo(VeiculoInterface $veiculo){
    return $veiculo->buzinar();
}

$carro = new Carro();
$moto = new Moto();

testarVeiculo($carro);
testarVeiculo($moto);
Enter fullscreen mode Exit fullscreen mode

Interfaces e Injeção de Dependência

No último exemplo, já adentramos uma outra técnica que anda de mãos dadas com interfaces, que é a Injeção de Dependência.

Passar uma interface como dependência é a essência da injeção de dependência.

A injeção de dependência é um padrão que fornece dependências externamente em vez de criá-las internamente. Isso torna o código mais testável, modular e flexível.

class TestadorDeVeiculos {
    private VeiculoInterface $veiculo;

    public function __construct(VeiculoInterface $veiculo){
         $this->veiculo = $veiculo;
    }

    public function testar(){
        $this->veiculo->buzinar();
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora, o TestadorDeVeiculos poderá testar qualquer classe que implemente o VeiculoInterface. Podemos criar caminhões, sedans, carrinho de golfe, etc. E o melhor de tudo? Não vamos precisar mexer em nada que não seja a nova classe que estamos implementando, poderoso não?

Considerações Finais

O uso da herança pode ser tentador, mas na maioria dos casos, a composição é uma escolha mais inteligente. Embora possa gerar um pouco mais de código inicial, ela oferece mais flexibilidade, reduz o acoplamento e facilita a manutenção do sistema.

É importante não demonizar a herança, ela ainda tem seu lugar em casos específicos, como sistemas legados com código altamente repetitivo, mas para projetos modernos, a composição é a chave para um código mais limpo e sustentável.

Que tal revisar seu projeto e ver onde a composição pode substituir a herança?

Top comments (0)