Existem duas escolas de pensamento sobre a melhor forma de estender, aprimorar e reutilizar o código em um sistema orientado a objetos:
Herança: estenda a funcionalidade de uma classe criando uma subclasse. Substitua membros da superclasse nas subclasses para fornecer nova funcionalidade. Torne os métodos abstratos / virtuais para forçar as subclasses a "preencher os espaços em branco" quando a superclasse deseja uma interface específica, mas é independente da sua implementação.
Agregação: crie uma nova funcionalidade participando de outras classes e combinando-as em uma nova classe. Anexe uma interface comum a essa nova classe para interoperabilidade com outro código.
Quais são os benefícios, custos e consequências de cada um? Existem outras alternativas?
Eu vejo esse debate surgindo regularmente, mas acho que ainda não foi perguntado no Stack Overflow (embora haja alguma discussão relacionada). Também há uma surpreendente falta de bons resultados no Google.
fonte
Respostas:
Não é uma questão de qual é o melhor, mas de quando usar o quê.
Nos casos "normais", basta uma pergunta simples para descobrir se precisamos de herança ou agregação.
No entanto, existe uma grande área cinzenta. Então, precisamos de vários outros truques.
Para encurtar. Devemos usar a agregação se parte da interface não for usada ou precisar ser alterada para evitar uma situação ilógica. Só precisamos usar herança, se precisarmos de quase toda a funcionalidade sem grandes alterações. E em caso de dúvida, use Agregação.
Outra possibilidade para o caso de termos uma classe que precisa de parte da funcionalidade da classe original é dividir a classe original em uma classe raiz e uma subclasse. E deixe a nova classe herdar da classe raiz. Mas você deve tomar cuidado com isso, para não criar uma separação ilógica.
Vamos adicionar um exemplo. Temos uma classe 'Dog' com métodos: 'Eat', 'Walk', 'Bark', 'Play'.
Agora precisamos de uma classe 'Gato', que precisa de 'Comer', 'Andar', 'Purr' e 'Brincar'. Então, primeiro tente estendê-lo de um cão.
Parece, tudo bem, mas espere. Este gato pode latir (os amantes de gatos me matam por isso). E um gato latindo viola os princípios do universo. Portanto, precisamos substituir o método Bark para que ele não faça nada.
Ok, isso funciona, mas cheira mal. Então, vamos tentar uma agregação:
Ok, isso é legal. Este gato não late mais, nem mesmo em silêncio. Mas ainda tem um cachorro interno que quer sair. Então, vamos tentar a solução número três:
Isso é muito mais limpo. Não há cães internos. E gatos e cães estão no mesmo nível. Podemos até introduzir outros animais de estimação para estender o modelo. A menos que seja um peixe ou algo que não anda. Nesse caso, precisamos novamente refatorar. Mas isso é algo para outra hora.
fonte
makeSound
e deixaria cada subclasse implementar seu próprio tipo de som. Isso ajudará em situações em que você tem muitos animais de estimação e apenas o fazfor_each pet in the list of pets, pet.makeSound
.No início do GOF, eles declaram
Isso é discutido mais adiante aqui
fonte
A diferença é normalmente expressa como a diferença entre "é um" e "tem um". A herança, a relação "é um", é resumida com agrado no Princípio da Substituição de Liskov . A agregação, o relacionamento "has a", é apenas isso - mostra que o objeto agregador possui um dos objetos agregados.
Também existem distinções adicionais - a herança privada em C ++ indica que um relacionamento "é implementado em termos de", que também pode ser modelado pela agregação de objetos membros (não expostos).
fonte
Aqui está o meu argumento mais comum:
Em qualquer sistema orientado a objetos, existem duas partes para qualquer classe:
Sua interface : a "face pública" do objeto. Esse é o conjunto de recursos que anuncia para o resto do mundo. Em muitos idiomas, o conjunto está bem definido em uma "classe". Geralmente, essas são as assinaturas de método do objeto, embora isso varie um pouco de acordo com o idioma.
Sua implementação : o trabalho "por trás das cenas" que o objeto faz para satisfazer sua interface e fornecer funcionalidade. Normalmente, são os dados de código e membro do objeto.
Um dos princípios fundamentais do OOP é que a implementação é encapsulada (ou seja: oculta) dentro da classe; a única coisa que pessoas de fora devem ver é a interface.
Quando uma subclasse herda de uma subclasse, normalmente herda tanto a implementação ea interface. Por sua vez, isso significa que você é forçado a aceitar ambos como restrições à sua classe.
Com a agregação, você escolhe a implementação ou a interface ou ambas - mas não é forçado a participar. A funcionalidade de um objeto é deixada para o próprio objeto. Pode adiar para outros objetos como quiser, mas é responsável por si próprio. Na minha experiência, isso leva a um sistema mais flexível: um que é mais fácil de modificar.
Portanto, sempre que estou desenvolvendo software orientado a objetos, quase sempre prefiro agregação a herança.
fonte
Eu dei uma resposta para "É um" vs "Tem um": qual é o melhor? .
Basicamente, concordo com outras pessoas: use herança apenas se sua classe derivada realmente for do tipo que você está estendendo, não apenas porque ela contém os mesmos dados. Lembre-se de que herança significa que a subclasse ganha os métodos e os dados.
Faz sentido para a sua classe derivada ter todos os métodos da superclasse? Ou você simplesmente promete a si mesmo que esses métodos devem ser ignorados na classe derivada? Ou você se vê substituindo métodos da superclasse, tornando-os não operacionais, para que ninguém os chame inadvertidamente? Ou dando dicas para sua ferramenta de geração de documentos da API para omitir o método do documento?
Essas são pistas fortes de que a agregação é a melhor escolha nesse caso.
fonte
Vejo muitas respostas "é-a vs. tem-a; elas são conceitualmente diferentes" sobre esta e as questões relacionadas.
A única coisa que encontrei na minha experiência é que tentar determinar se um relacionamento é "é-a" ou "tem-a" está fadado ao fracasso. Mesmo que você possa fazer corretamente essa determinação para os objetos agora, a alteração dos requisitos significa que você provavelmente estará errado em algum momento no futuro.
Outra coisa que descobri é que é muito difícil converter de herança em agregação, uma vez que há muito código escrito em torno de uma hierarquia de herança. Mudar de uma superclasse para uma interface significa alterar quase todas as subclasses do sistema.
E, como mencionei em outras partes deste post, a agregação tende a ser menos flexível que a herança.
Portanto, você tem uma tempestade perfeita de argumentos contra a herança sempre que precisar escolher um ou outro:
Assim, tenho a tendência de escolher agregação - mesmo quando parece haver um forte relacionamento é-um.
fonte
A pergunta é normalmente formulada como Composição vs. Herança , e já foi feita aqui antes.
fonte
Eu queria fazer disso um comentário sobre a pergunta original, mas 300 caracteres mordem [; <).
Eu acho que precisamos ter cuidado. Primeiro, existem mais sabores do que os dois exemplos bastante específicos feitos na pergunta.
Além disso, sugiro que é valioso não confundir o objetivo com o instrumento. Quer-se ter certeza de que a técnica ou metodologia escolhida apóia a consecução do objetivo principal, mas não considero muito fora do contexto qual discussão é a melhor técnica é muito útil. Ajuda a conhecer as armadilhas das diferentes abordagens, juntamente com seus pontos positivos e claros.
Por exemplo, o que você deseja realizar, o que você tem disponível para começar e quais são as restrições?
Você está criando uma estrutura de componentes, mesmo uma de finalidade especial? As interfaces são separáveis das implementações no sistema de programação ou são realizadas por uma prática usando um tipo diferente de tecnologia? Você pode separar a estrutura de herança das interfaces (se houver) da estrutura de herança das classes que as implementam? É importante ocultar a estrutura de classes de uma implementação do código que depende das interfaces que a implementação oferece? Existem várias implementações para serem usadas ao mesmo tempo ou a variação é mais prolongada como conseqüência da manutenção e do aprimoramento? Isso e muito mais precisam ser considerados antes de você se fixar em uma ferramenta ou metodologia.
Por fim, é tão importante fixar distinções na abstração e como você pensa disso (como é um versus versus tem) a diferentes recursos da tecnologia OO? Talvez sim, se mantiver a estrutura conceitual consistente e gerenciável para você e outras pessoas. Mas é aconselhável não ser escravizado por isso e pelas contorções que você pode acabar fazendo. Talvez seja melhor recuar um nível e não ser tão rígido (mas deixe uma boa narração para que outros possam dizer o que está acontecendo). [Eu procuro o que torna uma parte específica de um programa explicável, mas algumas vezes busco elegância quando há uma vitória maior. Nem sempre a melhor ideia.]
Sou um purista da interface e sou atraído pelos tipos de problemas e abordagens em que o purismo da interface é apropriado, seja criando uma estrutura Java ou organizando algumas implementações COM. Isso não o torna apropriado para tudo, nem mesmo para tudo, mesmo que eu jure por isso. (Eu tenho alguns projetos que parecem fornecer contra-exemplos sérios contra o purismo da interface, por isso será interessante ver como eu consigo lidar com isso.)
fonte
Vou cobrir a parte em que estes podem ser aplicados. Aqui está um exemplo de ambos, em um cenário de jogo. Suponha que haja um jogo com diferentes tipos de soldados. Cada soldado pode ter uma mochila que pode conter coisas diferentes.
Herança aqui? Há uma boina marinha, verde e um franco-atirador. Estes são tipos de soldados. Portanto, há um soldado de classe base com Marine, Boina Verde e Sniper como classes derivadas
Agregação aqui? A mochila pode conter granadas, armas (tipos diferentes), faca, medikit, etc. Um soldado pode ser equipado com qualquer um desses itens a qualquer momento, além de também ter um colete à prova de balas que age como armadura quando atacado e lesão diminui para uma certa porcentagem. A classe soldado contém um objeto da classe colete à prova de balas e a classe mochila, que contém referências a esses itens.
fonte
Eu acho que não é um debate de ou / ou. É só isso:
Ambos têm o seu lugar, mas a herança é mais arriscada.
Embora, é claro, não faria sentido ter uma classe Shape 'tendo-um' Point e um quadrado. Aqui a herança é devida.
As pessoas tendem a pensar primeiro na herança quando tentam projetar algo extensível, é isso que está errado.
fonte
O favor acontece quando os dois candidatos se qualificam. A e B são opções e você é a favor de A. O motivo é que a composição oferece mais possibilidades de extensão / flexibilidade do que generalização. Essa extensão / flexibilidade refere-se principalmente à flexibilidade de tempo de execução / dinâmica.
O benefício não é imediatamente visível. Para ver o benefício, você precisa aguardar a próxima solicitação de mudança inesperada. Portanto, na maioria dos casos, aqueles que aderiram à generlalização falham quando comparados aos que adotaram a composição (exceto um caso óbvio mencionado posteriormente). Daí a regra. Do ponto de vista do aprendizado, se você pode implementar uma injeção de dependência com êxito, deve saber qual delas favorecer e quando. A regra também ajuda você a tomar uma decisão; Se você não tiver certeza, selecione a composição.
Resumo: Composição: o acoplamento é reduzido apenas com algumas coisas menores que você conecta em algo maior, e o objeto maior chama o objeto menor de volta. Geração: Do ponto de vista da API, definir que um método possa ser substituído é um compromisso mais forte do que definir que um método pode ser chamado. (muito poucas ocasiões em que a generalização vence). E nunca esqueça que, com a composição, você também está usando herança, de uma interface em vez de uma grande classe
fonte
Ambas as abordagens são usadas para resolver problemas diferentes. Você nem sempre precisa agregar mais de duas ou mais classes ao herdar de uma classe.
Às vezes, você precisa agregar uma única classe porque essa classe está selada ou possui membros não virtuais que você precisa interceptar para criar uma camada de proxy que obviamente não é válida em termos de herança, mas contanto que a classe que você está executando proxy tem uma interface que você pode assinar, isso pode funcionar bastante bem.
fonte