Herança x Agregação [fechado]

151

Existem duas escolas de pensamento sobre a melhor forma de estender, aprimorar e reutilizar o código em um sistema orientado a objetos:

  1. 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.

  2. 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.

Craig Walker
fonte
9
Boa pergunta, infelizmente não tenho tempo suficiente agora.
Toon Krijthe 6/11/08
4
Uma boa resposta é melhor do que um mais rápido ... Eu estarei assistindo à minha própria pergunta, então eu vou votar-lo, pelo menos :-P
Craig Walker

Respostas:

181

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.

  • Se A nova classe é mais ou menos como a classe original. Use herança. A nova classe agora é uma subclasse da classe original.
  • Se a nova classe deve ter a classe original. Use agregação. A nova classe agora tem a classe original como membro.

No entanto, existe uma grande área cinzenta. Então, precisamos de vários outros truques.

  • Se usamos herança (ou pretendemos usá-la), mas usamos apenas parte da interface, ou somos forçados a substituir muitas funcionalidades para manter a correlação lógica. Temos um cheiro desagradável que indica que tivemos que usar agregação.
  • Se usamos agregação (ou pretendemos usá-la), mas descobrimos que precisamos copiar quase toda a funcionalidade. Então temos um cheiro que aponta na direção da herança.

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'.

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Agora precisamos de uma classe 'Gato', que precisa de 'Comer', 'Andar', 'Purr' e 'Brincar'. Então, primeiro tente estendê-lo de um cão.

class Cat is Dog
  Purr; 
end;

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.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, isso funciona, mas cheira mal. Então, vamos tentar uma agregação:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

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:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

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.

Toon Krijthe
fonte
5
A "reutilização de quase todas as funcionalidades de uma classe" é a única vez em que eu realmente prefiro a herança. O que eu realmente gostaria é de uma linguagem que possa dizer facilmente "delegar a esse objeto agregado para esses métodos específicos"; esse é o melhor dos dois mundos.
Craig Walker
Não tenho certeza sobre outras linguagens, mas o Delphi tem um mecanismo que permite que os membros implementem parte da interface.
Toon Krijthe
2
O objetivo C usa protocolos que permitem decidir se sua função é necessária ou opcional.
usar o seguinte código
1
Ótima resposta com um exemplo prático. Eu projetaria a classe Pet com um método chamado makeSounde 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 faz for_each pet in the list of pets, pet.makeSound.
blongho
38

No início do GOF, eles declaram

Favorecer a composição do objeto sobre a herança de classe.

Isso é discutido mais adiante aqui

kit de ferramentas
fonte
27

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).

Harper Shelby
fonte
Além disso, a diferença é resumida bem na própria pergunta a que você deve responder. Gostaria que as pessoas lessem primeiro as perguntas antes de começarem a traduzir. Vocês cegamente promovem padrões de design para se tornarem membros do clube de elite?
Val
Eu não gosto dessa abordagem, é mais sobre a composição
Alireza Rahmani Khalili
15

Aqui está o meu argumento mais comum:

Em qualquer sistema orientado a objetos, existem duas partes para qualquer classe:

  1. 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.

  2. 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.

Craig Walker
fonte
Eu acho que você usa uma classe abstrata para definir uma interface e, em seguida, usa herança direta para cada classe de implementação concreta (tornando-a bastante superficial). Qualquer agregação está sob a implementação concreta.
orcmid 6/11/08
Se você precisar de interfaces adicionais, retorne-as através de métodos na interface da interface abstrata do nível principal, enxágüe com água.
orcmid
Você acha que a agregação é melhor nesse cenário? Um livro é um SellingItem, um DigitalDisc é um SelliingItem codereview.stackexchange.com/questions/14077/…
LCJ
11

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.

Bill Karwin
fonte
6

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:

  1. Sua escolha provavelmente será a errada em algum momento
  2. Mudar essa escolha é difícil depois que você a escolhe.
  3. A herança tende a ser uma escolha pior, pois é mais restritiva.

Assim, tenho a tendência de escolher agregação - mesmo quando parece haver um forte relacionamento é-um.

Craig Walker
fonte
A evolução dos requisitos significa que seu projeto de OO estará inevitavelmente errado. Herança versus agregação é apenas a ponta desse iceberg. Você não pode arquitetar para todos os futuros possíveis; portanto, siga a idéia do XP: resolva os requisitos atuais e aceite que talvez você precise refatorar.
Bill Karwin
Eu gosto dessa linha de investigação e questionamento. Preocupado com o borrão é-a, tem-a e usa-a. Começo com interfaces (junto com a identificação das abstrações implementadas para as quais quero interfaces). O nível adicional de indireção é inestimável. Não siga sua conclusão de agregação.
precisa
Esse é o meu ponto, no entanto. Tanto a herança quanto a agregação são métodos para resolver o problema. Um desses métodos incorre em todos os tipos de penalidades quando você precisa resolver os problemas amanhã.
Craig Walker
Na minha opinião, agregação + interfaces são a alternativa à herança / subclasse; você não pode fazer polimorfismo baseado apenas na agregação.
Craig Walker
3

A pergunta é normalmente formulada como Composição vs. Herança , e já foi feita aqui antes.

Bill the Lizard
fonte
Certo, você é .. engraçado que SO não tenha dado essa como uma das questões relacionadas.
Craig Walker
2
SO procurar ainda suga grande momento
Vinko Vrsalovic
Acordado. Por isso não fechei isso. Isso, e muitas pessoas já haviam respondido aqui.
Bill Bill Lizard
1
SO precisa de um recurso para rolar 2 perguntas (e suas respostas) em 1
Craig Walker
3

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.)

orcmid
fonte
2

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.

Salman Kasbati
fonte
2

Eu acho que não é um debate de ou / ou. É só isso:

  1. Os relacionamentos is-a (herança) ocorrem com menos frequência do que os relacionamentos has-a (composição).
  2. A herança é mais difícil de acertar, mesmo quando é apropriado usá-la; portanto, é necessário tomar a devida diligência, pois pode quebrar o encapsulamento, incentivar o acoplamento rígido, expondo a implementação e assim por diante.

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.

Vinko Vrsalovic
fonte
1

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

Nuvens azuis
fonte
0

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.

cfeduke
fonte