Devo usar herança ou composição?
Devo usar herança ou composição? Sempre que você vê class {CLASS_NAME} extends {PARENT_CLASS_NAME}
, está diante de uma herança direta de classe, também conhecida como “herança clássica”, uma herança vertical onde as implementações e as especificações descem na “árvore”.
Um diagrama de um caso didático de herança:
Se levado para um pseudo-código que usa o paradigma de orientação a objetos, poderíamos ter:
class Animal
{
// TODO
}
class Gato extends Animal
{
// TODO
}
class Coelho extends Animal
{
// TODO
}
class Cachorro extends Animal
{
// TODO
}
A herança clássica nos leva as dois benefícios:
- Reuso de contrato (especificação);
- Reuso de código (implementação);
Se você já estudou orientação a objetos, em qualquer linguagem que seja, já deve ter “saturado” na mente a ideia de que a herança evita duplicação de código, uma vez que ela herda da classe pai suas partes concretas (implementações) e seus contratos/interfaces (especificações). No entanto, reuso de código por si só não é motivo para se usar herança. Reuso é consequência do uso de herança. Esse conceito é muito importante de se ter.
A herança faz sentido quando se tem a necessidade de representar um tipo, de tal forma que, quando essa classe é estendida, o seu tipo também é, podendo criar, inclusive, um novo sub-tipo.
No caso do nosso diagrama, temos a seguinte relação:
- Animal é a classe pai, o topo da hierarquia.
- Gato estende Animal, logo, Gato é do tipo Animal.
- Cachorro estende Animal, logo Cachorro é um Animal (só que mais específico);
Lidamos com uma inferência lógica: Gato estende Animal, logo, Gato também é um Animal.
Outra relação importante que temos na herança refere-se à generalização e à especificação. Quanto mais “alto” na árvore, mais genérico é, quanto mais baixo, mais específico. Generalizar é o ato de reunir características comuns numa classe de tal forma que ela possa servir de extensão para outras. No lado oposto, especificar é tornar específico, ou seja, com características mais singulares e que não devem estar na classe pai, pois, normalmente, são essas características que dão o valor de sub-tipo à classe em questão. A relação que temos é que, classes pai são classes de generalização e subclasses normalmente são classes de especialização.
Ainda dentro do nosso exemplo, a classe Animal deve ser, em teoria, a parte mais genérica e as subclasses Gato, Coelho e Cachorro devem ser mais específicas. Por exemplo, um método latir()
não poderia estar na classe Animal para ser estendido para as outras, uma vez que essa é uma característica específica dos cachorros.
A herança clássica é um mecanismo útil e não deve ser demonizada. O grande problema é o seu mau uso, e se dá, normalmente, quando precisamos trazer características de outra classe para a nossa e, nesse caso, a primeira coisa que costuma vir à mente é a opção de “herdá-la” ao invés de pensarmos em objetos que comunicam entre sí.
Se a nossa classe não é do tipo da outra, herança não deve ser utilizada. Quando estendemos uma classe, não herdamos apenas a sua implementação e sua especificação, herdamos também o seu tipo. Logo, se uma classe não é do tipo da outra, a herança não é o mecanismo adequado.
É comum esse tipo de construção:
class MySqlConnection
{
// TODO
}
class Usuario extends MySqlConnection
{
// TODO
}
A classe Usuario
estende a MySqlConnection
. Mas, Usuario
não é uma MySqlConnection
, não é do tipo MySqlConnection
. Esse tipo de erro é muito comum pois, como desenvolvedores, tendemos a achar que apenas a herança pode resolver os nossos problemas de reuso de código.
Esse exemplo, por sinal, é um caso para composição, de tal forma que saímos do contexto de “ser” para o contexto de “usar”. Usuario não deve ser MySqlConnection, ele precisa usar MySqlConnection.
Problemas que são evidenciados na super utilização de herança (em excesso):
- Mudar a classe pai pode afetar todas as classes filhas, mesmo quando isso não for intencional. O projeto fica um pouco menos previsível.
- O encapsulamento costuma ficar fraco (o acoplamento é forte, com uma tendo muito conhecimento sobre a outra, às vezes conhecimento até específico demais).
- A herança é um relacionamento estático, não é possível mudar em tempo de execução.
Composição
Antes, um disclaimer: Nesse artigo, quando eu falo em composição, eu quero dizer: um objeto que usa outro objeto independentemente da relação associativa entre eles. Aqui é pouco importante separar a relação (se é uma agregação simples ou uma agregação de composição etc).
A composição também é uma forma de extensão, só que pela delegação de trabalho para outro objeto. Diferente da herança clássica onde tipo, atributos e métodos são estendidos. Quando uma classe precisa usar o comportamento de outra, usualmente é melhor usar composição.
Em termos gerais, se o seu problema não é caso para herança clássica, ele será um caso para composição (sem entrar aqui na possibilidade de se usar traits, um mecanismo de herança horizontal disponível em algumas linguagens).
Alguns dos pontos positivos do uso de composição:
- Facilita a mudança da associação entre classes em tempo de execução;
- Permite que um objeto assuma múltiplos comportamentos com decisões em tempo de execução;
Voltando ao nosso exemplo, a melhor relação que podemos ter:
E, traduzindo para código, poderíamos ter essa implementação:
class MySqlConnection
{
// TODO
}
class Usuario
{
public function findById(int $id) : array
{
$connection = new MySqlConnection();
// TODO
}
}
Usuario não mais estende MySqlConnection, ao invés disso, ele instancia essa classe e utiliza o objeto resultante.
Mas, priorizando injeção de dependência e o princípio de inversão de dependência, a melhor alternativa seria construimos esse exemplo assim:
class MySqlConnection implements Connection
{
// TODO
}
class Usuario
{
protected $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function findById(int $id)
{
$this->connection->select('...');
// TODO
}
}
$connection = new MySqlConnection();
$myUser = new Usuario($connection);
Assim como no primeiro exemplo, nesse também temos composição (no sentido de um objeto A que usa um objeto B). Só mudamos a forma deles se relacionarem. No primeiro exemplo temos uma associação de agregação de composição e nesse, injetamos MySqlConnection como dependência de Usuario, numa associação de agregação (onde a parte – MySqlConnection – não depende do todo – Usuario – para existir).
Concluindo
Não faz sentido polarizar e afirmar que você deve usar uma em detrimento a outra. Composição e herança podem coexistir cada qual com seu caso de uso. Por outro lado, o uso de composição é sempre encorajado. Não exatamente no sentido de fazer o desenvolvedor “parar” de usar herança, mas no sentido de que, tendo mais objetos inter-relacionados (não no sentido de quantidade, mas no de qualidade de comportamento), as responsabilidades são melhores delegadas, favorece o reuso, a refatoração, a testabilidade e até mesmo a mudança de associação em tempo de execução.
Leia também: “Quatro habilidades para se tornar um desenvolvedor back-end”