"Preferir composição sobre herança" é apenas uma boa heurística
Você deve considerar o contexto, pois essa não é uma regra universal. Não pense que nunca use herança quando puder fazer composição. Se fosse esse o caso, você poderia corrigi-lo banindo a herança.
Espero deixar claro esse ponto ao longo deste post.
Não tentarei defender os méritos da composição por si só. O que considero fora de tópico. Em vez disso, falarei sobre algumas situações em que o desenvolvedor pode considerar a herança que seria melhor usar a composição. Nessa nota, a herança tem seus próprios méritos, os quais também considero fora de tópico.
Exemplo de veículo
Estou escrevendo sobre desenvolvedores que tentam fazer as coisas da maneira idiota, para fins narrativos
Vamos para uma variante dos exemplos clássicos que alguns cursos OOP usar ... Nós temos uma Vehicle
classe, então derivamos Car
, Airplane
, Balloon
e Ship
.
Nota : Se você precisar fundamentar este exemplo, finja que estes são tipos de objetos em um videogame.
Então, Car
e Airplane
pode ter algum código comum, porque ambos podem rolar sobre rodas em terra. Os desenvolvedores podem considerar a criação de uma classe intermediária na cadeia de herança para isso. No entanto, na verdade, também há algum código compartilhado entre Airplane
e Balloon
. Eles poderiam considerar criar outra classe intermediária na cadeia de herança para isso.
Assim, o desenvolvedor procuraria por herança múltipla. No ponto em que os desenvolvedores estão procurando por herança múltipla, o design já deu errado.
É melhor modelar esse comportamento como interfaces e composição, para que possamos reutilizá-lo sem precisar ter herança de várias classes. Se os desenvolvedores, por exemplo, criarem a FlyingVehicule
classe. Eles estariam dizendo que Airplane
é uma FlyingVehicule
(herança de classe), mas poderíamos dizer que Airplane
tem um Flying
componente (composição) e Airplane
é uma IFlyingVehicule
(herança de interface).
Usando interfaces, se necessário, podemos ter herança múltipla (de interfaces). Além disso, você não está acoplando a uma implementação específica. Aumentar a reutilização e a testabilidade do seu código.
Lembre-se de que a herança é uma ferramenta para o polimorfismo. Além disso, o polimorfismo é uma ferramenta para reutilização. Se você pode aumentar a reutilização do seu código usando a composição, faça-o. Se você não tiver certeza de que a composição fornece ou não melhor capacidade de reutilização, "Preferir composição à herança" é uma boa heurística.
Tudo isso sem mencionar Amphibious
.
De fato, podemos não precisar de coisas que decolam. Stephen Hurn tem um exemplo mais eloquente em seus artigos "Favor da composição sobre a herança", parte 1 e parte 2 .
Substitutibilidade e Encapsulamento
Deve A
herdar ou compor B
?
Se A
é uma especialização B
disso que deve cumprir o princípio de substituição de Liskov , a herança é viável, até desejável. Se houver situações em que A
não há uma substituição válida B
, não devemos usar herança.
Podemos estar interessados na composição como uma forma de programação defensiva, para defender a classe derivada . Em particular, quando você começar a usar B
para outros fins, pode haver pressão para alterar ou estendê-lo para que seja mais adequado para esses fins. Se houver o risco de B
expor métodos que possam resultar em um estado inválido A
, devemos usar a composição em vez da herança. Mesmo se somos os autores de ambos B
e A
, é uma coisa a menos com que se preocupar, pois a composição facilita a reutilização B
.
Podemos até argumentar que, se houver recursos B
que A
não sejam necessários (e não sabemos se esses recursos podem resultar em um estado inválido para A
, seja na presente implementação ou no futuro), é uma boa ideia usar a composição em vez de herança.
A composição também tem as vantagens de permitir a troca de implementações e facilitar a zombaria.
Nota : há situações em que queremos usar a composição, apesar da substituição ser válida. Arquivamos essa substituibilidade usando interfaces ou classes abstratas (qual usar quando é outro tópico) e depois usamos a composição com injeção de dependência da implementação real.
Finalmente, é claro, há o argumento de que devemos usar a composição para defender a classe pai, porque a herança quebra o encapsulamento da classe pai:
A herança expõe uma subclasse a detalhes da implementação de seus pais, é comum dizer que 'a herança quebra o encapsulamento'
- Padrões de projeto: elementos de software orientado a objetos reutilizáveis, Gang of Four
Bem, essa é uma classe pai mal projetada. É por isso que você deve:
Projete para herança ou proíba-a.
- Java eficaz, Josh Bloch
Problema de ioiô
Outro caso em que a composição ajuda é o problema do ioiô . Esta é uma citação da Wikipedia:
No desenvolvimento de software, o problema do ioiô é um antipadrão que ocorre quando um programador precisa ler e entender um programa cujo gráfico de herança é tão longo e complicado que o programador precisa continuar alternando entre muitas definições diferentes de classe para seguir o fluxo de controle do programa.
Você pode resolver, por exemplo: Sua classe C
não herdará da classe B
. Em vez disso, sua classe C
terá um membro do tipo A
, que pode ou não ser (ou ter) um objeto do tipo B
. Dessa forma, você não estará programando com os detalhes de implementação B
, mas com o contrato que a interface (de) A
oferece.
Exemplos de contadores
Muitas estruturas favorecem a herança sobre a composição (que é o oposto do que estamos discutindo). O desenvolvedor pode fazer isso porque eles colocam muito trabalho em sua classe base que implementá-lo com composição aumentaria o tamanho do código do cliente. Às vezes, isso ocorre devido a limitações do idioma.
Por exemplo, uma estrutura ORM do PHP pode criar uma classe base que usa métodos mágicos para permitir a gravação de código como se o objeto tivesse propriedades reais. Em vez disso, o código manipulado pelos métodos mágicos vai para o banco de dados, consulta o campo solicitado específico (talvez o armazene em cache para solicitação futura) e o retorna. Fazer isso com composição exigiria que o cliente criasse propriedades para cada campo ou escrevesse alguma versão do código dos métodos mágicos.
Adendo : Existem outras maneiras de permitir a extensão de objetos ORM. Portanto, não acho que a herança seja necessária neste caso. É mais barato.
Por outro exemplo, um mecanismo de videogame pode criar uma classe base que usa código nativo, dependendo da plataforma de destino para fazer renderização em 3D e manipulação de eventos. Este código é complexo e específico da plataforma. Seria caro e passível de erro para o usuário desenvolvedor do mecanismo lidar com esse código, de fato, isso faz parte do motivo do uso do mecanismo.
Além disso, sem a parte de renderização 3D, é quantas estruturas de widget funcionam. Isso evita que você se preocupe em lidar com mensagens do sistema operacional ... de fato, em muitos idiomas, você não pode escrever esse código sem alguma forma de restrição nativa. Além disso, se você o fizesse, aumentaria sua portabilidade. Em vez disso, com herança, desde que o desenvolvedor não quebre a compatibilidade (demais); você poderá portar facilmente seu código para quaisquer novas plataformas suportadas no futuro.
Além disso, considere que muitas vezes queremos substituir apenas alguns métodos e deixar todo o resto com as implementações padrão. Se estivéssemos usando a composição, teríamos que criar todos esses métodos, mesmo que apenas para delegar ao objeto empacotado.
Por esse argumento, há um ponto em que a composição pode ser pior para manutenção do que herança (quando a classe base é muito complexa). No entanto, lembre-se de que a manutenção da herança pode ser pior do que a da composição (quando a árvore de herança é muito complexa), a qual me refiro no problema do ioiô.
Nos exemplos apresentados, os desenvolvedores raramente pretendem reutilizar o código gerado por herança em outros projetos. Isso mitiga a reutilização diminuída do uso de herança em vez de composição. Além disso, usando a herança, os desenvolvedores da estrutura podem fornecer muito código fácil de usar e fácil de descobrir.
Pensamentos finais
Como você pode ver, a composição tem alguma vantagem sobre a herança em algumas situações, nem sempre. É importante considerar o contexto e os diferentes fatores envolvidos (como reutilização, manutenção, testabilidade, etc.) para tomar a decisão. Voltando ao primeiro ponto: "Preferir composição sobre herança" é apenas uma boa heurística.
Você também pode perceber que muitas das situações que descrevo podem ser resolvidas com Traits ou Mixins até certo ponto. Infelizmente, esses recursos não são comuns na grande lista de idiomas e geralmente vêm com algum custo de desempenho. Felizmente, seus métodos populares Extension Extension e Default Implementations atenuam algumas situações.
Tenho um post recente em que falo sobre algumas das vantagens das interfaces, por que exigimos interfaces entre UI, negócios e acesso a dados em C # . Ajuda a dissociar e facilita a reutilização e a testabilidade, você pode estar interessado.