De onde vem esse conceito de “favorecer composição sobre herança”?

138

Nos últimos meses, o mantra "favorecer a composição sobre a herança" parece ter surgido do nada e se tornar quase um tipo de meme dentro da comunidade de programação. E toda vez que eu vejo, estou um pouco confuso. É como se alguém dissesse "favor treinar sobre martelos". Na minha experiência, composição e herança são duas ferramentas diferentes com diferentes casos de uso, e tratá-las como se fossem intercambiáveis e uma inerentemente superior à outra não faz sentido.

Além disso, eu nunca vejo uma explicação real para por que a herança é ruim e a composição é boa, o que me deixa mais suspeito. É suposto ser apenas aceito na fé? A substituição de Liskov e o polimorfismo têm benefícios claros e bem conhecidos, e a OMI compreende todo o ponto de usar programação orientada a objetos, e ninguém explica por que eles devem ser descartados em favor da composição.

Alguém sabe de onde vem esse conceito e qual é a razão por trás dele?

    
por Mason Wheeler 05.04.2011 / 00:54
fonte

14 respostas

131

Embora eu ache que já ouvi discussões de composição versus herança muito antes do GoF, não consigo identificar uma fonte específica. Poderia ter sido Booch de qualquer maneira.

< rant >

Ah, mas como muitos mantras, este se degenerou ao longo das linhas típicas:

  1. é apresentado com uma explicação detalhada e argumento por uma fonte bem respeitada que cunha a frase de efeito como um lembrete da discussão complexa original
  2. é compartilhada com uma pequena piscadinha do conhecimento do clube por alguns conhecedores por um tempo, geralmente ao comentar erros do n00b
  3. em breve, ela é repetida sem pensar por milhares e milhares de pessoas que nunca leram a explicação, mas amam usá-la como desculpa para não pensar e como uma forma barata e fácil de se sentir superior a outras
  4. eventualmente, nenhuma quantidade razoável de desmascaramento pode conter a maré do "meme" - e o paradigma degenera em religião e dogma.

O meme, originalmente destinado a levar os nbs a iluminar, é agora usado como um clube para espancá-los inconscientes.

Composição e herança são coisas muito diferentes e não devem ser confundidas entre si. Embora seja verdade que a composição possa ser usada para simular herança com muito trabalho extra , isso não torna a herança um cidadão de segunda classe, nem torna a composição o filho favorito. O fato de muitos n00bs tentarem usar a herança como um atalho não invalida o mecanismo, e quase todos os n00bs aprendem com o erro e, assim, melhoram.

PENSE sobre seus designs e pare de usar slogans.

< / rant >

    
por 19.04.2012 / 03:55
fonte
72

Experiência.

Como você diz, eles são ferramentas para trabalhos diferentes, mas a frase surgiu porque as pessoas não estavam usando dessa forma.

A herança é basicamente uma ferramenta polimórfica, mas algumas pessoas, para seu perigo posterior, tentam usá-la como uma maneira de reutilizar / compartilhar código. O raciocínio é "bem se eu herdo então eu recebo todos os métodos de graça", mas ignorando o fato de que essas duas classes potencialmente não têm relação polimórfica.

Então, por que favorecer a composição sobre a herança - bem, simplesmente porque, mais frequentemente, a relação entre as classes não é polimórfica. Ele existe simplesmente para ajudar a lembrar as pessoas de não responderem por herança.

    
por 05.04.2011 / 06:40
fonte
41

Não é uma ideia nova, acredito que tenha sido introduzida no livro de padrões de projeto do GoF, publicado em 1994.

O principal problema com a herança é que é uma caixa branca. Por definição, você precisa conhecer os detalhes da implementação da classe que está herdando. Com a composição, por outro lado, você só se importa com a interface pública da turma que está compondo.

Do livro do GoF:

Inheritance exposes a subclass to details of its parent's implementation, it's often said that 'inheritance breaks encapsulation'

O artigo da wikipedia sobre o livro GoF tem uma introdução decente.

    
por 05.04.2011 / 01:08
fonte
26

Para responder parte da sua pergunta, acredito que este conceito apareceu pela primeira vez nos Padrões de Design: Elementos de Orientação a Objetos Reutilizáveis do GOF Software , publicado pela primeira vez em 1994. A frase aparece no topo da página 20, na introdução:

Favor object composition over inheritance

Eles iniciam essa declaração com uma breve comparação de herança com composição. Eles não dizem "nunca usam herança".

    
por 05.04.2011 / 01:07
fonte
22

"Composição sobre herança" é uma maneira curta (e aparentemente enganosa) de dizer "Ao sentir que os dados (ou comportamento) de uma classe devem ser incorporados em outra classe, considere sempre usar a composição antes de aplicar cegamente a herança". / p>

Por que isso é verdade? Porque a herança cria um acoplamento rígido em tempo de compilação entre as duas classes. A composição, ao contrário, é o acoplamento flexível, que, entre outros, permite a separação clara das preocupações, a possibilidade de alternar dependências em tempo de execução e a testabilidade de dependência mais fácil e isolada.

Isso significa apenas que a herança deve ser tratada com cuidado, porque tem um custo, não que não seja útil. Na verdade, "Composição sobre herança" geralmente acaba sendo "Composição + herança sobre herança", já que muitas vezes você quer que sua dependência composta seja uma superclasse abstrata em vez da própria subclasse concreta. Ele permite que você alterne entre diferentes implementações concretas de sua dependência em tempo de execução.

Por esse motivo (entre outros), você provavelmente verá a herança usada com mais frequência na forma de implementação de interface ou classes abstratas do que a herança baunilha.

Um exemplo (metafórico) poderia ser:

"Eu tenho uma classe Snake e quero incluir como parte dessa classe o que acontece quando o Snake morde. Eu ficaria tentado a fazer com que o Snake herde uma classe BiterAnimal que tenha o método Bite () e sobrescreva esse método para Mas Composição sobre Herança me avisa que eu deveria tentar usar a composição ... No meu caso, isso poderia ser traduzido em Serpente tendo um membro Bite.A classe Bite poderia ser abstrata (ou uma interface) com várias subclasses. Isso me permitiria coisas agradáveis como ter VenomousBite e DryBite subclasses e ser capaz de mudar mordida na mesma instância serpente como a cobra cresce de idade. Além disso, manipulação de todos os efeitos de uma mordida em sua própria classe separada poderia permitir-me para reutilizá-la em aquela classe Frost, porque a gelada morde, mas não é um BiterAnimal, e assim por diante ... "

    
por 06.04.2011 / 17:15
fonte
22

Alguns argumentos possíveis para composição:

A composição é um pouco mais agnóstica em termos de linguagem / estrutura
A herança e o que ela impõe / requer / habilita diferem entre idiomas em termos do que a sub / superclasse tem acesso e quais implicações de desempenho podem ter em relação a métodos virtuais etc. A composição é bem básica e requer muito pouco suporte a idiomas e, portanto, implementações em diferentes plataformas / frameworks podem compartilhar padrões de composição mais facilmente.

A composição é uma maneira muito simples e tátil de construir objetos
A herança é relativamente fácil de entender, mas ainda não é facilmente demonstrada na vida real. Muitos objetos na vida real podem ser divididos em partes e compostos. Digamos que uma bicicleta pode ser construída usando duas rodas, uma estrutura, um assento, uma corrente etc. Facilmente explicada pela composição. Enquanto em uma metáfora de herança você poderia dizer que uma bicicleta estende um monociclo, um tanto viável, mas ainda muito mais distante da imagem real do que da composição (obviamente este não é um bom exemplo de herança, mas o ponto permanece o mesmo). Mesmo a palavra herança (pelo menos da maioria dos falantes de inglês dos EUA eu esperaria) automaticamente invoca um significado ao longo das linhas "Algo transmitido de um parente falecido" que tem alguma correlação com o seu significado em software, mas ainda apenas se encaixa vagamente. p>

A composição é quase sempre mais flexível
Usando a composição, você sempre pode escolher definir seu próprio comportamento ou simplesmente expor essa parte de suas partes compostas. Desta forma, você não enfrenta nenhuma das restrições que podem ser impostas por uma hierarquia de herança (virtual versus não-virtual, etc.)

Assim, pode ser porque Composição é naturalmente uma metáfora mais simples que tem menos restrições teóricas do que herança. Além disso, essas razões específicas podem ser mais aparentes durante o tempo de design, ou possivelmente se destacar quando lidamos com alguns dos pontos problemáticos da herança.

Isenção de responsabilidade:
Obviamente não é este claro / one way street. Cada projeto merece avaliação de vários padrões / ferramentas. A herança é amplamente utilizada, tem muitos benefícios e muitas vezes é mais elegante que a composição. Estas são apenas algumas das possíveis razões que podem ser usadas quando se favorece a composição.

    
por 07.02.2013 / 21:00
fonte
10

Talvez você tenha notado pessoas dizendo isso nos últimos meses, mas ele é conhecido por bons programadores há muito mais tempo do que isso. Eu certamente tenho dito isso quando apropriado por cerca de uma década.

O ponto do conceito é que existe uma grande sobrecarga conceitual para a herança. Quando você está usando herança, cada chamada de método tem um despacho implícito. Se você tiver árvores de herança profundas, ou várias expedições, ou (ainda pior) ambas, então descobrir onde o método específico irá despachar para qualquer chamada em particular pode se tornar uma PITA real. Isso torna o raciocínio correto sobre o código mais complexo e dificulta a depuração.

Deixe-me dar um exemplo simples para ilustrar. Suponha que, no fundo de uma árvore de herança, alguém nomeou um método foo . Então alguém aparece e adiciona foo no topo da árvore, mas fazendo algo diferente. (Este caso é mais comum com herança múltipla.) Agora que a pessoa que trabalha na classe raiz quebrou a obscura classe filha e provavelmente não percebe isso. Você poderia ter 100% de cobertura com testes de unidade e não perceber essa quebra porque a pessoa no topo não pensaria em testar a classe filha, e os testes para a classe filha não pensam em testar os novos métodos criados no topo . (É certo que existem maneiras de escrever testes de unidade que irão detectar isso, mas também há casos em que você não pode facilmente escrever testes dessa maneira.)

Por outro lado, quando você usa composição, em cada chamada, geralmente fica mais claro para o que você está enviando a chamada. (OK, se você estiver usando inversão de controle, por exemplo, com injeção de dependência, então descobrir onde a chamada vai também pode ficar problemático. Mas geralmente é mais simples de descobrir.) Isso torna o raciocínio mais fácil. Como um bônus, a composição resulta em métodos segregados uns dos outros. O exemplo acima não deve acontecer lá porque a classe filho se moveria para algum componente obscuro, e nunca há uma dúvida sobre se a chamada para foo foi destinada ao componente obscuro ou ao objeto principal.

Agora você está absolutamente certo de que herança e composição são duas ferramentas muito diferentes que servem dois tipos diferentes de coisas. Claro que a herança carrega a sobrecarga conceitual, mas quando é a ferramenta certa para o trabalho, ela carrega menos sobrecarga conceitual do que tentar não usá-la e fazer à mão o que ela faz por você. Ninguém que saiba o que está fazendo diria que você nunca deve usar herança. Mas tenha certeza de que é a coisa certa a fazer.

Infelizmente, muitos desenvolvedores aprendem sobre software orientado a objetos, aprendem sobre herança e, em seguida, usam seu novo machado sempre que possível. O que significa que eles tentam usar herança, onde a composição era a ferramenta certa. Espero que eles aprendam melhor com o tempo, mas freqüentemente isso não acontece até depois de alguns membros removidos, etc. Dizendo a eles que é uma má ideia que tende a acelerar o processo de aprendizado e reduzir lesões.

    
por 05.04.2011 / 01:30
fonte
8

É uma reação à observação de que iniciantes em OO tendem a usar herança quando não precisam. A herança certamente não é uma coisa ruim, mas pode ser usada em excesso. Se uma classe precisa apenas de funcionalidade de outra, a composição provavelmente funcionará. Em outras circunstâncias, a herança funcionará e a composição não.

Herdar de uma classe implica muitas coisas. Implica que um Derivado é um tipo de Base (veja o Princípio da Substituição de Liskov para os detalhes sangrentos), em que sempre que você usar uma base faria sentido usar um derivado. Fornece o acesso Derivado aos membros protegidos e funções membro do Base. É um relacionamento próximo, o que significa que tem alto acoplamento, e as mudanças em um são mais propensas a exigir mudanças no outro.

O acoplamento é uma coisa ruim. Isso torna os programas mais difíceis de entender e modificar. Outras coisas sendo iguais, você deve sempre selecionar a opção com menos acoplamento.

Portanto, se a composição ou a herança fizerem o trabalho com eficiência, escolha composição, porque é um acoplamento menor. Se a composição não fizer o trabalho efetivamente, e a herança for, escolha a herança, porque você precisa.

    
por 05.04.2011 / 16:55
fonte
8

Aqui estão meus dois centavos (além de todos os pontos excelentes já levantados):

IMHO, tudo se resume ao fato de que a maioria dos programadores realmente não obtém herança e acaba exagerando, parcialmente como resultado de como esse conceito é ensinado. Esse conceito existe como uma maneira de tentar dissuadi-los de exagerar, em vez de se concentrar em ensiná-los a fazer isso da maneira certa.

Qualquer pessoa que tenha passado tempo ensinando ou orientando sabe que isso é o que acontece com frequência, especialmente com novos desenvolvedores que têm experiência em outros paradigmas:

Esses desenvolvedores inicialmente acham que herança é esse conceito assustador. Então eles acabam criando tipos com sobreposições de interface (por exemplo, o mesmo comportamento especificado sem subtipagem comum) e com globais para implementar partes comuns de funcionalidade.

Então (geralmente como resultado de um ensino excessivamente zeloso), eles aprendem sobre os benefícios da herança, mas geralmente são ensinados como a solução completa para reutilização. Eles acabam percebendo que qualquer comportamento compartilhado deve ser compartilhado por herança. Isso ocorre porque o foco geralmente está na herança da implementação, e não na subtipagem.

Em 80% dos casos, isso é bom o suficiente. Mas os outros 20% são onde o problema começa. Para evitar a reescrita e garantir que eles tenham aproveitado a implementação de compartilhamento, eles começam a projetar sua hierarquia em torno da implementação pretendida, em vez das abstrações subjacentes. A "Pilha herdada da lista duplamente vinculada" é um bom exemplo disso.

A maioria dos professores não insiste em introduzir o conceito de interfaces neste ponto, porque é um outro conceito, ou porque (em C ++) você tem que fingir usando classes abstratas e herança múltipla que não é ensinada neste estágio. Em Java, muitos professores não distinguem a "não herança múltipla" ou "herança múltipla é má" da importância das interfaces.

Tudo isso é agravado pelo fato de que aprendemos muito sobre a beleza de não ter que escrever código supérfluo com herança de implementação, que toneladas de código de delegação simples não parecem naturais. Então a composição parece confusa, e é por isso que precisamos dessas regras de polegares para forçar os novos programadores a usá-los de qualquer maneira (o que eles exageram também).

    
por 05.04.2011 / 17:26
fonte
8

Em um dos comentários, Mason mencionou que um dia estaríamos falando sobre Herança considerada prejudicial .

Espero que sim.

O problema com a herança é ao mesmo tempo simples e mortal, não respeita a ideia de que um conceito deve ter uma funcionalidade.

Na maioria das linguagens OO, ao herdar de uma classe base você:

  • herda de sua interface
  • herda de sua implementação (dados e métodos)

E aqui se torna o problema.

Esta não é a única abordagem, apesar de as línguas 00 estarem mais presas nela. Felizmente existem interfaces / classes abstratas naquelas.

Além disso, a falta de facilidade para fazer isso contribui para torná-lo amplamente usado: francamente, mesmo sabendo disso, você herdaria de uma interface e incorporaria a classe base por composição, delegando a maioria das chamadas de método? No entanto, seria consideravelmente melhor se você fosse avisado se, de repente, um novo método surgisse na interface e tivesse que escolher, conscientemente, como implementá-lo.

Como um contraponto, Haskell apenas permite usar o Princípio de Liskov quando "derivando" de interfaces puras (chamadas conceitos) (1). Você não pode derivar de uma classe existente, apenas a composição permite que você incorpore seus dados.

(1) conceitos podem fornecer um padrão sensível para uma implementação, mas como eles não têm dados, esse padrão só pode ser definido em termos dos outros métodos propostos pelo conceito ou em termos de constantes.

    
por 06.04.2011 / 19:14
fonte
7

A resposta simples é: herança tem maior acoplamento do que composição. Dadas duas opções, de qualidades equivalentes, escolha aquela que é menos acoplada.

    
por 05.04.2011 / 22:27
fonte
7

Acho que esse tipo de conselho é como dizer "prefira dirigir até voar". Ou seja, os aviões têm todos os tipos de vantagens sobre os carros, mas isso vem com uma certa complexidade. Então, se muitas pessoas tentam voar do centro da cidade para os subúrbios, o conselho que elas realmente precisam ouvir é que elas não precisam voar, e, de fato, voar vai apenas fazer isso. mais complicado a longo prazo, mesmo que pareça legal / eficiente / fácil a curto prazo. Considerando que quando você faz precisa voar, geralmente deve ser óbvio.

Da mesma forma, a herança pode fazer coisas que a composição não pode, mas você deve usar isso quando precisar, e não quando não precisar. Então, se você nunca se sente tentado a supor que precisa de hereditariedade quando não o faz, então não precisa do conselho de "preferir composição". Mas muitas pessoas fazem , e fazem precisam desse conselho.

É implícito que quando você realmente precisa de herança, é óbvio, e você deve usá-lo então.

Além disso, a resposta de Steven Lowe. Realmente, realmente isso.

    
por 26.11.2012 / 18:58
fonte
2

A herança não é inerentemente ruim, e a composição não é inerentemente boa. Eles são meramente ferramentas que um programador OO pode usar para projetar software.

Quando você olha para uma classe, ela está fazendo mais do que deveria ( SRP )? Ele está duplicando a funcionalidade desnecessariamente ( DRY ) ou está muito interessado nas propriedades ou métodos de outras classes ( Inveja do recurso ) ?. Se a classe está violando todos esses conceitos (e possivelmente mais), ela está tentando ser uma Classe de Deus . Estes são vários problemas que podem ocorrer durante a criação de software, nenhum dos quais é necessariamente um problema de herança, mas que pode rapidamente criar grandes dores de cabeça e dependências frágeis onde o polimorfismo também foi aplicado.

A raiz do problema provavelmente é menos a falta de compreensão sobre herança, e mais uma de má escolha em termos de design, ou talvez não reconhecer "cheiros de código" relacionados a classes que não estão aderindo ao Princípio da Responsabilidade Única . O Polimorfismo e Substituição Liskov não precisam ser descartados em favor da composição. O próprio polimorfismo pode ser aplicado sem depender da herança, todos esses conceitos são complementares. Se aplicado com cuidado. O truque é procurar manter seu código simples, limpo e não sucumbir ao excesso de preocupação com o número de classes que você precisa criar para criar um design de sistema robusto.

Na medida em que a questão de favorecer a composição sobre a herança, este é realmente apenas mais um caso da aplicação ponderada dos elementos de design que fazem mais sentido em termos do problema a ser resolvido. Se você não precisa herdar o comportamento, então provavelmente você não deve, como composição, ajudar a evitar incompatibilidades e grandes esforços de refatoração mais tarde. Se, por outro lado, você descobrir que está repetindo muito código, de modo que toda a duplicação esteja centralizada em um grupo de classes semelhantes, pode ser que criar um ancestral comum ajudaria a reduzir o número de chamadas e classes idênticas talvez seja necessário repetir entre cada aula. Assim, você está favorecendo a composição, mas não está assumindo que a herança nunca é aplicável.

    
por 19.04.2012 / 05:29
fonte
-1
A citação atual vem, creio eu, de "Effective Java" de Joshua Bloch, onde é o título de um dos capítulos. Ele alega que a herança é segura dentro de um pacote e onde quer que uma classe tenha sido especificamente projetada e documentada para extensão. Ele alega, como outros notaram, que a herança quebra o encapsulamento porque uma subclasse depende dos detalhes de implementação de sua superclasse. Isso leva, diz ele, à fragilidade.

Embora ele não mencione isso, parece-me que a herança única em Java significa que a composição oferece muito mais flexibilidade do que a herança.

    
por 05.04.2011 / 23:03
fonte