Nos últimos meses, o mantra "favorece a composição sobre a herança" parece ter surgido do nada e se tornou quase algum tipo de meme na comunidade de programação. E toda vez que vejo, fico um pouco confuso. É como se alguém dissesse "brocas favoritas 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 fosse inerentemente superior à outra não faz sentido.
Além disso, nunca vejo uma explicação real sobre por que a herança é ruim e a composição é boa, o que me deixa mais desconfiada. Deveria ser aceito apenas com fé? A substituição e o polimorfismo de Liskov têm benefícios bem conhecidos e bem definidos, e a IMO compreende todo o ponto de usar a programação orientada a objetos, e ninguém nunca explica por que eles devem ser descartados em favor da composição.
Alguém sabe de onde vem esse conceito e qual é a lógica por trás dele?
fonte
Respostas:
Embora eu ache que já ouvi discussões sobre 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 degenerou de acordo com linhas típicas:
O meme, originalmente destinado a levar n00bs à iluminação, 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 uma com a outra. 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 atalho não invalida o mecanismo, e quase todos os n00bs aprendem com o erro e, assim, melhoram.
Por favor PENSAR sobre seus projetos, e parar de jorrar slogans.
</rant>
fonte
Experiência.
Como você diz, elas são ferramentas para trabalhos diferentes, mas a frase surgiu porque as pessoas não a estavam usando dessa maneira.
A herança é primariamente uma ferramenta polimórfica, mas algumas pessoas, muito mais tarde, tentam usá-la como uma maneira de reutilizar / compartilhar código. A lógica é "bem, se eu herdar, 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, na maioria das vezes, o relacionamento entre classes não é polimórfico. Ele existe simplesmente para ajudar a lembrar as pessoas a não reagirem por herança.
fonte
Não é uma idéia nova, acredito que foi realmente introduzida no livro de padrões de design do GoF, publicado em 1994.
O principal problema da herança é que é uma caixa branca. Por definição, você precisa conhecer os detalhes de implementação da classe da qual está herdando. Com a composição, por outro lado, você se preocupa apenas com a interface pública da classe que está compondo.
Do livro GoF:
O artigo da wikipedia no livro do GoF tem uma introdução decente.
fonte
Para responder a parte de sua pergunta, acredito que esse conceito apareceu pela primeira vez no livro GOF Design Patterns: Elements of Reusable Oriented Object Software , publicado pela primeira vez em 1994. A frase aparece na parte superior da página 20, na introdução:
Eles precedem esta afirmação com uma breve comparação entre herança e composição. Eles não dizem "nunca use herança".
fonte
"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 a outra classe, sempre considere usar a composição antes de aplicar cegamente a herança".
Por que isso é verdade? Porque a herança cria um acoplamento apertado em tempo de compilação entre as 2 classes. A composição, ao contrário, é um acoplamento frouxo, o que, entre outros, permite uma separação clara de preocupações, a possibilidade de alternar dependências em tempo de execução e uma testabilidade de dependências mais fácil e mais isolada.
Isso significa apenas que a herança deve ser tratada com cuidado, pois 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", pois muitas vezes você deseja que sua dependência composta seja uma superclasse abstrata e não a própria subclasse concreta. Permite alternar 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 de baunilha.
Um exemplo (metafórico) poderia ser:
"Eu tenho uma classe Snake e quero incluir como parte dessa classe o que acontece quando a Snake morde. Eu ficaria tentado a fazer com que a Snake herde uma classe BiterAnimal que possui o método Bite () e substitua esse método para refletir a mordida venenosa. Mas o Composition over Inheritance me avisa que eu deveria tentar usar a composição ... No meu caso, isso pode se traduzir no fato de o Snake ter um membro Bite.A classe Bite pode ser abstrata (ou uma interface) com várias subclasses. coisas boas como ter as subclasses VenomousBite e DryBite e ser capaz de mudar a mordida na mesma instância Snake à medida que a cobra envelhece.Além disso, lidar com todos os efeitos de uma mordida em sua própria classe separada pode permitir que eu a reutilize nessa classe Frost , porque a geada morde, mas não é um BiterAnimal, e assim por diante ... "
fonte
Alguns argumentos possíveis para composição:
A composição é um pouco mais de
herança independente da linguagem / estrutura, e o que ela impõe / requer / habilita diferirá entre os idiomas em termos de acesso à sub / superclasse e quais implicações de desempenho podem ter métodos virtuais errados, etc. A composição é bastante básica e requer muito pouco suporte a idiomas e, portanto, implementações em diferentes plataformas / estruturas 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 é tão facilmente demonstrada na vida real. Muitos objetos na vida real podem ser divididos em partes e compostos. Digamos que uma bicicleta possa ser construída usando duas rodas, uma estrutura, um assento, uma corrente etc. Explicado facilmente pela composição. Enquanto em uma metáfora da 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 exemplo muito bom de herança, mas o ponto permanece o mesmo). Até a palavra herança (pelo menos a maioria dos falantes de inglês dos EUA, eu esperaria) invoca automaticamente um significado ao longo das linhas "Algo passado de um parente falecido", que tem alguma correlação com seu significado no software, mas ainda se encaixa pouco a pouco.
A composição é quase sempre mais flexível
Ao usar a composição, você sempre pode definir seu próprio comportamento ou simplesmente expor essa parte de suas partes compostas. Dessa 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 a composição é naturalmente uma metáfora mais simples que possui menos restrições teóricas do que herança. Além disso, esses motivos específicos podem ser mais aparentes durante o tempo de design ou possivelmente se destacarem ao lidar com alguns dos pontos críticos da herança.
Isenção de responsabilidade:
Obviamente não é essa rua clara / de sentido único. Cada projeto merece avaliação de vários padrões / ferramentas. A herança é amplamente usada, tem muitos benefícios e muitas vezes é mais elegante que a composição. Estas são apenas algumas das razões possíveis que se pode usar ao favorecer a composição.
fonte
Talvez você tenha notado pessoas dizendo isso nos últimos meses, mas isso é conhecido por bons programadores há muito mais tempo. Eu certamente digo isso quando apropriado há cerca de uma década.
O objetivo do conceito é que existe uma grande sobrecarga conceitual para a herança. Quando você está usando herança, cada chamada de método possui um despacho implícito. Se você possui árvores de herança profundas, ou despacho múltiplo, ou (pior ainda) ambos, então descobrir para onde o método específico será despachado em qualquer chamada específica pode se tornar uma PITA real. Torna o raciocínio correto sobre o código mais complexo e torna a depuração mais difícil.
Deixe-me dar um exemplo simples para ilustrar. Suponha que, no fundo de uma árvore de herança, alguém nomeie um método
foo
. Então alguém aparece e acrescentafoo
no topo da árvore, mas fazendo algo diferente. (Esse caso é mais comum com herança múltipla.) Agora essa pessoa que trabalha na classe raiz quebrou a classe filho obscura e provavelmente não a percebe. 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 filho, e os testes para a classe filho não pensariam em testar os novos métodos criados no topo . (É certo que existem maneiras de escrever testes de unidade que capturam isso, mas também há casos em que você não pode escrever facilmente testes dessa maneira.)Por outro lado, ao usar a composição, a cada chamada geralmente fica mais claro para o qual você está enviando a chamada. (OK, se você estiver usando inversão de controle, por exemplo, com injeção de dependência, descobrir para onde a chamada vai também pode ser problemático. Mas geralmente é mais fácil descobrir.) Isso facilita o raciocínio. Como bônus, a composição resulta em métodos segregados um do outro. O exemplo acima não deve acontecer lá, porque a classe filho mudaria para algum componente obscuro, e nunca há uma dúvida sobre se a chamada
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 a dois tipos diferentes de coisas. A herança segura carrega uma sobrecarga conceitual, mas quando é a ferramenta certa para o trabalho, carrega menos sobrecarga conceitual do que tentar não usá-la e fazer manualmente o que ela faz por você. Ninguém que sabe o que está fazendo diria que você nunca deve usar herança. Mas certifique-se de que é a coisa certa a fazer.
Infelizmente, muitos desenvolvedores aprendem sobre software orientado a objetos, aprendem sobre herança e depois usam seu novo machado o mais rápido possível. O que significa que eles tentam usar a herança onde a composição era a ferramenta certa. Espero que eles aprendam melhor com o tempo, mas frequentemente isso não acontece até depois de alguns membros removidos, etc. Dizendo-lhes de antemão que é uma má ideia tende a acelerar o processo de aprendizado e reduzir os ferimentos.
fonte
É uma reação à observação de que os iniciantes em OO tendem a usar herança quando não precisam. 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. Isso implica que Derivado é um tipo de Base (consulte o princípio de Substituição de Liskov para obter detalhes sangrentos), pois sempre que você usa uma Base, faria sentido usar uma Derivada. Dá acesso derivado aos membros protegidos e funções de membro do Base. É um relacionamento íntimo, o que significa que possui um alto acoplamento e as alterações em uma são mais propensas a exigir alterações na outra.
Acoplamento é uma coisa ruim. Isso torna os programas mais difíceis de entender e modificar. Sendo outras coisas iguais, você sempre deve selecionar a opção com menos acoplamento.
Portanto, se a composição ou a herança farão o trabalho com eficiência, escolha a composição, porque é um acoplamento menor. Se a composição não realizar o trabalho de maneira eficaz e a herança escolher, escolha herança, porque você precisa.
fonte
Aqui estão meus dois centavos (além de todos os excelentes pontos já levantados):
IMHO, tudo se resume ao fato de que a maioria dos programadores realmente não recebe 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 o certo.
Qualquer pessoa que tenha passado algum tempo ensinando ou mentorizando sabe que é isso 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. Portanto, 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 mais abrangente para reutilizar. Eles acabam com a percepção de 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 reescrever e garantir que eles tenham aproveitado o compartilhamento da implementação, eles começam a projetar sua hierarquia em torno da implementação pretendida, em vez das abstrações subjacentes. A "Pilha herda da lista duplamente vinculada" é um bom exemplo disso.
A maioria dos professores não insiste em introduzir o conceito de interfaces neste momento, porque é outro conceito ou porque (em C ++) você precisa falsificá-los usando aulas abstratas e herança múltipla que não é ensinada nesta fase. Em Java, muitos professores não distinguem a "herança múltipla" ou "herança múltipla é ruim" da importância das interfaces.
Tudo isso é agravado pelo fato de que aprendemos muito da beleza de não precisar escrever código supérfluo com herança de implementação, que toneladas de código de delegação simples parecem antinaturais. Portanto, a composição parece confusa, e é por isso que precisamos dessas regras práticas para pressionar os novos programadores a usá-las de qualquer maneira (que também exageram).
fonte
Em um dos comentários, Mason mencionou que um dia estaríamos falando sobre herança considerada prejudicial .
Acredito que sim.
O problema da herança é ao mesmo tempo simples e mortal, não respeita a ideia de que um conceito deve ter uma funcionalidade.
Na maioria dos idiomas OO, ao herdar de uma classe base, você:
E aqui se torna o problema.
Essa não é a única abordagem, embora 00 idiomas estejam presos a ela. Felizmente, existem interfaces / classes abstratas.
Além disso, a falta de facilidade para fazer o contrário contribui para torná-lo amplamente utilizado: 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? Seria consideravelmente melhor, você seria avisado se de repente um novo método surgisse na interface e tivesse que escolher, conscientemente, como implementá-lo.
Como contraponto,
Haskell
permita apenas o uso do Princípio de Liskov ao "derivar" de interfaces puras (chamadas conceitos) (1). Você não pode derivar de uma classe existente, apenas a composição permite incorporar seus dados.(1) conceitos podem fornecer um padrão sensato 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.
fonte
A resposta simples é: herança tem maior acoplamento que composição. Dadas duas opções, de qualidades equivalentes, escolha a que for menos acoplada.
fonte
Eu acho que esse tipo de conselho é como dizer "prefiro dirigir a voar". Ou seja, os aviões têm todo tipo de vantagens sobre os carros, mas isso vem com uma certa complexidade. Portanto, se muitas pessoas tentam voar do centro da cidade para os subúrbios, o conselho que elas realmente precisam ouvir é que não precisam voar e, de fato, voar apenas tornará as coisas mais complicadas a longo prazo, mesmo que pareça legal / eficiente / fácil a curto prazo. Considerando que, quando você não precisa voar, é geralmente suposto ser óbvio.
Da mesma forma, a herança pode fazer coisas que a composição não pode, mas você deve usá-lo quando precisar, e não quando não precisar. Portanto, se você nunca é tentado a assumir que precisa de herança quando não precisa, não precisa do conselho de "preferir composição". Mas muitas pessoas fazem , e fazer precisam esse conselho.
Deveria estar implícito que, quando você realmente precisa de herança, é óbvio, e você deve usá-la.
Além disso, a resposta de Steven Lowe. Realmente, realmente isso.
fonte
A herança não é inerentemente ruim e a composição não é inerentemente boa. São apenas ferramentas que um programador de OO pode usar para projetar software.
Quando você olha para uma classe, ela está fazendo mais do que deveria ( SRP )? Está duplicando a funcionalidade desnecessariamente ( DRY ) ou está excessivamente interessado nas propriedades ou métodos de outras classes ( Feature Envy ) ?. Se a classe está violando todos esses conceitos (e possivelmente mais), ela está tentando ser uma classe de Deus . Existem vários problemas que podem ocorrer ao projetar software, nenhum dos quais é necessariamente um problema de herança, mas que pode criar rapidamente grandes dores de cabeça e dependências frágeis nas quais o polimorfismo também foi aplicado.
A raiz do problema provavelmente será menos a falta de entendimento sobre herança e mais uma das más escolhas em termos de design, ou talvez não o reconhecimento de "odores de código" relacionados a classes que não estão aderindo ao Princípio de Responsabilidade Única . O polimorfismo e a substituição de Liskov não precisam ser descartados em favor da composição. O próprio polimorfismo pode ser aplicado sem depender de herança, todos esses são conceitos bastante complementares. Se aplicado com cuidado. O truque é manter o código simples, limpo e não sucumbir à preocupação excessiva 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, esse é realmente apenas outro caso da aplicação cuidadosa dos elementos de design que fazem mais sentido em termos de solução do problema. Se você não precisa herdar o comportamento, provavelmente não deve, pois a composição ajudará a evitar incompatibilidades e grandes esforços de refatoração posteriormente. Se, por outro lado, você descobrir que está repetindo muito código, de modo que toda a duplicação está centrada em um grupo de classes semelhantes, pode ser que a criação de um ancestral comum ajude a reduzir o número de chamadas e classes idênticas pode ser 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.
fonte
A citação real vem, acredito, do "Java Efetivo" de Joshua Bloch, onde é o título de um dos capítulos. Ele afirma 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 afirma, como outros observaram, 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.
fonte