Esta página defende composição sobre herança com o seguinte argumento (reformulado em minhas palavras):
Uma alteração na assinatura de um método da superclasse (que não foi substituída na subclasse) causa alterações adicionais em muitos lugares quando usamos Herança. No entanto, quando usamos o Composition, a alteração adicional necessária ocorre apenas em um único local: a subclasse.
Essa é realmente a única razão para favorecer a composição sobre a herança? Porque, se esse for o caso, esse problema pode ser facilmente aliviado através da aplicação de um estilo de codificação que advoga a substituição de todos os métodos da superclasse, mesmo que a subclasse não altere a implementação (ou seja, colocando substituições fictícias na subclasse). Estou faltando alguma coisa aqui?
Respostas:
Eu gosto de analogias, então aqui está uma: você já viu uma daquelas TVs que tinham um videocassete embutido? Que tal aquele com um videocassete e um DVD player? Ou um que possua Blu-ray, DVD e videocassete. Ah, e agora todo mundo está transmitindo, então precisamos criar um novo aparelho de TV ...
Provavelmente, se você possui um aparelho de TV, não possui um como o acima. Provavelmente, a maioria nunca existiu. Você provavelmente tem um monitor ou conjunto com inúmeras entradas, para que não precise de um novo conjunto sempre que houver um novo tipo de dispositivo de entrada.
A herança é como a TV com o videocassete embutido. Se você tem 3 tipos diferentes de comportamentos que deseja usar juntos e cada um possui 2 opções de implementação, você precisa de 8 classes diferentes para representar todas elas. À medida que você adiciona mais opções, os números explodem. Se você usar a composição, evite esse problema combinatório e o design tenderá a ser mais extensível.
fonte
Não não é. Na minha experiência, composição sobre herança é resultado de pensar no seu software como um monte de objetos com comportamentos que se comunicam / objetos que prestam serviços um ao outro em vez de classes e dados .
Dessa forma, você tem alguns objetos que fornecem serviços. Você os usa como blocos de construção (muito parecidos com o Lego) para permitir outro comportamento (por exemplo, um UserRegistrationService usa os serviços fornecidos por um EmailerService e um UserRepository ).
Ao construir sistemas maiores com essa abordagem, naturalmente usei herança em muito poucos casos. No final do dia, a herança é uma ferramenta muito poderosa que deve ser usada com cautela e somente quando apropriado.
fonte
"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 derivamosCar
,Airplane
,Balloon
eShip
.Nota : Se você precisar fundamentar este exemplo, finja que estes são tipos de objetos em um videogame.
Então,
Car
eAirplane
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 entreAirplane
eBalloon
. 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 queAirplane
é umaFlyingVehicule
(herança de classe), mas poderíamos dizer queAirplane
tem umFlying
componente (composição) eAirplane
é umaIFlyingVehicule
(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 comporB
?Se
A
é uma especializaçãoB
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 queA
não há uma substituição válidaB
, 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 deB
expor métodos que possam resultar em um estado inválidoA
, devemos usar a composição em vez da herança. Mesmo se somos os autores de ambosB
eA
, é uma coisa a menos com que se preocupar, pois a composição facilita a reutilizaçãoB
.Podemos até argumentar que, se houver recursos
B
queA
não sejam necessários (e não sabemos se esses recursos podem resultar em um estado inválido paraA
, 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:
- 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:
- 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:
Você pode resolver, por exemplo: Sua classe
C
não herdará da classeB
. Em vez disso, sua classeC
terá um membro do tipoA
, que pode ou não ser (ou ter) um objeto do tipoB
. Dessa forma, você não estará programando com os detalhes de implementaçãoB
, 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.
fonte
Normalmente, a idéia principal da composição por herança é fornecer maior flexibilidade de design e não reduzir a quantidade de código que você precisa alterar para propagar uma alteração (que considero questionável).
O exemplo na wikipedia dá um exemplo melhor do poder de sua abordagem:
Definindo uma interface base para cada "peça de composição", você pode facilmente criar vários "objetos compostos" com uma variedade que pode ser difícil de alcançar apenas com herança.
fonte
A linha: "Preferir composição sobre herança" não passa de uma cutucada na direção certa. Isso não vai te salvar da sua própria estupidez e, na verdade, lhe dá uma chance de tornar as coisas muito piores. Isso porque há muito poder aqui.
A principal razão pela qual isso ainda precisa ser dito é porque os programadores são preguiçosos. Tão preguiçosos que tomarão qualquer solução que signifique menos digitação no teclado. Esse é o principal ponto de venda da herança. Eu digito menos hoje e alguém se ferra amanhã. Então o que, eu quero ir para casa.
No dia seguinte, o chefe insiste em usar composição. Não explica o porquê, mas está insistindo nisso. Portanto, tomo uma instância do que eu estava herdando, exponho uma interface idêntica ao que eu estava herdando e implemento essa interface delegando todo o trabalho àquilo que eu estava herdando. É tudo digitação com morte cerebral que poderia ter sido feita com uma ferramenta de refatoração e parece inútil para mim, mas o chefe queria, então eu o faço.
No dia seguinte, pergunto se era isso que se queria. O que você acha que o chefe diz?
A menos que houvesse alguma necessidade de poder alterar dinamicamente (em tempo de execução) o que estava sendo herdado (veja o padrão de estado), isso era uma enorme perda de tempo. Como o chefe pode dizer isso e ainda ser a favor da composição sobre a herança?
Além disso, não fazer nada para impedir a quebra devido a alterações na assinatura do método, isso perde totalmente a maior vantagem da composição. Ao contrário da herança, você é livre para criar uma interface diferente . Mais um apropriado para como será usado neste nível. Você sabe, abstração!
Existem algumas pequenas vantagens em substituir automaticamente a herança pela composição e delegação (e algumas desvantagens), mas manter o cérebro desligado durante esse processo está perdendo uma grande oportunidade.
Indirecionar é muito poderoso. Use com sabedoria.
fonte
Aqui está outro motivo: em muitas linguagens OO (estou pensando em linguagens como C ++, C # ou Java aqui), não há como alterar a classe de um objeto depois que você o criou.
Suponha que estamos criando um banco de dados de funcionários. Temos uma classe abstrata de funcionários básica e começamos a derivar novas classes para funções específicas - engenheiro, segurança, motorista de caminhão e assim por diante. Cada classe tem um comportamento especializado para esse papel. Tudo é bom.
Então, um dia, um engenheiro é promovido a gerente de engenharia. Você não pode reclassificar um engenheiro existente. Em vez disso, você deve escrever uma função que crie um gerente de engenharia, copiando todos os dados do engenheiro, excluindo o engenheiro do banco de dados e adicionando o novo gerente de engenharia. E você precisa escrever uma função para cada possível mudança de função.
Pior ainda, suponha que um motorista de caminhão fique de licença médica de longo prazo e um segurança se ofereça para fazer o serviço de caminhão em tempo parcial para preencher. Agora você precisa inventar uma nova classe de segurança e motorista de caminhão apenas para eles.
Facilita muito a vida a criação de uma classe Employee concreta e a trata como um contêiner no qual você pode adicionar propriedades, como a função do trabalho.
fonte
A herança fornece duas coisas:
1) Reutilização de código: pode ser realizado facilmente através da composição. E, de fato, é melhor alcançado através da composição, pois preserva melhor o encapsulamento.
2) Polimorfismo: Pode ser realizado através de "interfaces" (classes em que todos os métodos são totalmente virtuais).
Um argumento forte para o uso de herança sem interface, é quando o número de métodos necessários é grande e sua subclasse deseja apenas alterar um pequeno subconjunto deles. No entanto, em geral, sua interface deve ter pegadas muito pequenas se você se inscrever no princípio de "responsabilidade única" (você deve), portanto, esses casos devem ser raros.
Ter o estilo de codificação que exige a substituição de todos os métodos da classe pai não evita escrever um grande número de métodos de passagem, então por que não reutilizar o código através da composição e obter o benefício adicional do encapsulamento? Além disso, se a superclasse fosse estendida adicionando outro método, o estilo de codificação exigiria a adição desse método a todas as subclasses (e há uma boa chance de estarmos violando a "segregação de interface" ). Esse problema cresce rapidamente quando você começa a ter vários níveis de herança.
Finalmente, em muitos idiomas o teste de unidade de objetos baseados em "composição" é muito mais fácil do que os objetos baseados em "herança" de teste de unidade, substituindo o objeto membro por um objeto mock / test / dummy.
fonte
Não.
Há muitas razões para favorecer a composição sobre a herança. Não existe uma única resposta correta. Mas vou mostrar outro motivo para favorecer a composição sobre a herança:
Favorecer a composição sobre a herança melhora a legibilidade do seu código , o que é muito importante ao trabalhar em um projeto grande com várias pessoas.
Aqui está como eu penso sobre isso: quando estou lendo o código de outra pessoa, se vejo que eles estenderam uma classe, vou perguntar que comportamento na superclasse eles estão mudando?
E então eu examinarei o código deles, procurando uma função substituída. Se não vejo, classifico isso como um uso indevido de herança. Eles deveriam ter usado a composição, apenas para me salvar (e todas as outras pessoas da equipe) de 5 minutos de leitura do código!
Aqui está um exemplo concreto: em Java, a
JFrame
classe representa uma janela. Para criar uma janela, você cria uma instânciaJFrame
e, em seguida, chama funções nessa instância para adicionar itens a ela e mostrá-la.Muitos tutoriais (mesmo os oficiais) recomendam que você estenda
JFrame
para poder tratar instâncias de sua classe como instâncias deJFrame
. Mas imho (e a opinião de muitos outros) é que esse é um péssimo hábito! Em vez disso, você deve apenas criar uma instância deJFrame
e chamar funções nessa instância. Não há absolutamente nenhuma necessidade de extensãoJFrame
neste caso, pois você não está alterando nenhum dos comportamentos padrão da classe pai.Por outro lado, uma maneira de executar pintura personalizada é estender a
JPanel
classe e substituir apaintComponent()
função. Este é um bom uso de herança. Se eu estiver examinando seu código e perceber que você estendeuJPanel
e substituiu apaintComponent()
função, saberei exatamente qual comportamento você está mudando.Se você escrever apenas um código sozinho, pode ser tão fácil usar a herança quanto usar a composição. Mas finja que você está em um ambiente de grupo e que outras pessoas estarão lendo seu código. Favorecer a composição sobre a herança facilita a leitura do código.
fonte
new
palavra-chave fornece uma. De qualquer forma, obrigado pela discussão.