Code Smell: Abuso de herança [duplicado]

49

Tem sido geralmente aceito na comunidade OO que se deve "favorecer composição sobre herança". Por outro lado, a herança fornece tanto o polimorfismo quanto uma maneira direta e concisa de delegar tudo a uma classe base, a menos que seja explicitamente substituída e, portanto, extremamente conveniente e útil. A delegação pode frequentemente (embora nem sempre) ser detalhada e quebradiça.

O mais óbvio sinal de abuso de herança do IMHO é a violação do Princípio de substituição de Liskov . Quais são alguns outros sinais de que a herança é a ferramenta errada para o trabalho, mesmo que pareça conveniente?

    
por dsimcha 16.10.2010 / 19:27
fonte

9 respostas

42

Ao herdar apenas para obter funcionalidade, você provavelmente está abusando da herança. Isso leva à criação de um God Object .

Herança em si não é um problema, desde que você veja uma relação real entre as classes (como os exemplos clássicos, como Dog estende Animal) e você não está colocando métodos na classe pai que não faz sentido em alguns de seus filhos (por exemplo, adicionar um método Bark () na classe Animal não faria sentido em uma classe Fish que estenda Animal).

Se uma classe precisa de funcionalidade, use a composição (talvez injetando o provedor de funcionalidade no construtor?). Se uma classe precisar ser como outra, use a herança.

    
por 16.10.2010 / 20:19
fonte
36

Eu diria, correndo o risco de ser abatido, que a herança é um cheiro de código em si:)

O problema com a herança é que ela pode ser usada para duas finalidades ortogonais:

  • interface (para polimorfismo)
  • implementação (para reutilização de código)

Ter um único mecanismo para obter os dois é o que leva ao "abuso de herança", já que a maioria das pessoas espera que a herança seja sobre a interface, mas pode ser usada para obter implementação padrão mesmo por programadores cuidadosos (é apenas tão fácil ignorar isso ...)

Na verdade, línguas modernas como Haskell ou Go abandonaram a herança para separar ambas as preocupações.

Voltar à faixa:

Uma violação do Princípio de Liskov é, portanto, o sinal mais seguro, pois significa que a parte "interface" do contrato não é respeitada.

No entanto, mesmo quando a interface é respeitada, você pode ter objetos herdados de classes base "gordas" apenas porque um dos métodos foi considerado útil.

Portanto, o Princípio de Liskov em si não é suficiente, o que você precisa saber é se o polimorfismo é usado ou não. Se não for, então não há muito sentido em herdar, em primeiro lugar, isso é um abuso.

Resumo:

  • aplica o Liskov Principle
  • verifique se é realmente usado

Uma maneira de contornar:

Impor uma separação clara de preocupações:

  • só herda de interfaces
  • use a composição para delegar a implementação

significa que pelo menos algumas teclas são necessárias para cada método, e de repente as pessoas começam a pensar se é ou não uma ótima idéia reutilizar essa classe "gorda" para apenas uma pequena parte dela.

    
por 16.10.2010 / 20:24
fonte
13

É realmente "geralmente aceito"? É uma ideia que ouvi algumas vezes, mas dificilmente é um princípio universalmente reconhecido. Como você acabou de apontar, herança e polimorfismo são as marcas registradas da OOP. Sem eles, você acaba de programar procedimentalmente com uma sintaxe pateta.

A composição é uma ferramenta para criar objetos de uma determinada maneira. Herança é uma ferramenta para construir objetos de uma maneira diferente. Não há nada de errado com nenhum deles; você só precisa saber como eles funcionam e quando é apropriado usar cada estilo.

    
por 16.10.2010 / 19:47
fonte
5

A força da herança é a reutilização. De interface, dados, comportamento, colaborações. É um padrão útil a partir do qual transmitir atributos para novas classes.

A desvantagem de usar herança é reusabilidade. A interface, dados e comportamento são encapsulados e transmitidos em massa, tornando-se uma proposta de tudo ou nada. Os problemas tornam-se especialmente evidentes à medida que as hierarquias se tornam mais profundas, à medida que as propriedades que podem ser úteis no abstrato tornam-se menos e começam a obscurecer as propriedades em nível local.

Todas as classes que usam o objeto de composição precisarão reimplementar ou não o mesmo código para interagir com ele. Embora essa duplicação exija alguma violação da DRY, ela oferece maior liberdade para os designers escolherem e escolherem as propriedades de uma classe mais significativa para seu domínio. Isto é particularmente vantajoso quando as classes se tornam mais especializadas.

Em geral, deve-se preferir criar classes por meio da composição e, em seguida, converter em herança aquelas classes com a maior parte de suas propriedades em comum, deixando as diferenças como composições.

IOW, YAGNI (você não precisará de herança).

    
por 16.10.2010 / 20:21
fonte
3

"Favor composição sobre herança" é a mais estúpida das afirmações. Ambos são elementos essenciais da OOP. É como dizer "favor martelos sobre serras".

É claro que a herança pode ser abusada, qualquer recurso de linguagem pode ser abusado, declarações condicionais podem ser abusadas.

    
por 19.03.2011 / 04:47
fonte
1

Talvez o mais óbvio seja quando a classe herdada acaba com uma pilha de métodos / propriedades que nunca são usados na interface exposta. Nos piores casos, onde a classe base tem membros abstratos, você encontrará implementações vazias (ou sem sentido) dos membros abstratos apenas para 'preencher os espaços em branco', por assim dizer.

Mais sutilmente, às vezes você vê onde, em um esforço para aumentar a reutilização de código, duas coisas que têm implementações semelhantes foram espremidas em uma implementação - apesar dessas duas coisas não estarem relacionadas. Isso tende a aumentar o acoplamento de código e desvendá-lo quando a semelhança é apenas uma coincidência pode ser bastante dolorosa, a menos que seja detectada antes.

Atualizado com alguns exemplos: Embora não seja diretamente relacionado à programação, acho que o melhor exemplo para esse tipo de coisa é a localização / internacionalização. Em inglês, há uma palavra para "sim". Em outras línguas pode haver muitos, muitos mais para concordar gramaticalmente com a questão. Alguém poderia dizer, ah, vamos ter uma string para "sim" e isso é bom para muitos idiomas. Então eles percebem que a string "sim" realmente não corresponde a "sim", mas na verdade o conceito de "resposta afirmativa a essa pergunta" - o fato de que é "sim" o tempo todo é uma coincidência e o tempo economizado apenas ter um "sim" é completamente perdido em desemaranhar a confusão resultante.

Eu vi um exemplo particularmente desagradável disso em nossa base de código onde o que era realmente um pai - > relacionamento filho em que pai e filho tinham propriedades com nomes semelhantes foi modelado com herança. Ninguém notou isso como tudo parecia estar funcionando, então o comportamento da criança (na verdade, base) e pai (subclasse) precisava ir em direções diferentes. Por sorte, isso aconteceu exatamente na hora errada, quando um prazo estava se aproximando - na tentativa de ajustar o modelo aqui e ali, a complexidade (tanto em termos de lógica quanto de duplicação de código) passou imediatamente. Também era muito difícil raciocinar sobre / trabalhar com, pois o código simplesmente não se relacionava com o modo como o negócio pensava ou falava sobre os conceitos que essas classes representavam. No final, tivemos que morder a bala e fazer algumas refatorações importantes que poderiam ter sido completamente evitadas se o cara original não tivesse decidido que algumas propriedades parecidas com valores que eram iguais representavam o mesmo conceito.

    
por 16.10.2010 / 19:47
fonte
1

Se você está subclassificando A para a classe B , mas ninguém se preocupa se B herda de A ou não, então esse é o sinal de que você pode estar errado.

    
por 17.10.2010 / 18:20
fonte
0

Existem situações em que a herança deve ser favorecida em relação à composição, e a distinção é muito mais clara do que uma questão de estilo. Estou parafraseando as Normas de Codificação C ++ de Sutter e Alexandrescu aqui, pois minha cópia está na minha estante de livros no momento. Leitura altamente recomendada, a propósito.

Em primeiro lugar, a herança é desencorajada quando desnecessária porque cria um relacionamento muito íntimo e estreitamente acoplado entre classes base e derivadas que podem causar dores de cabeça no caminho para manutenção. Não é apenas a opinião de um cara que a sintaxe da herança dificulta a leitura ou algo assim.

Dito isto, a herança é encorajada quando você quer que a própria classe derivada seja reutilizada em contextos onde a classe base está sendo usada atualmente, sem ter que modificar o código naquele contexto. Por exemplo, se você tem código que está começando a parecer com if (type1) type1.foo(); else if (type2) type2.foo(); , provavelmente você tem uma boa chance de observar a herança.

Em resumo, se você quiser apenas reutilizar os métodos da classe base, use composição. Se você quiser usar uma classe derivada no código que atualmente usa uma classe base, use herança.

    
por 19.03.2011 / 05:31
fonte
0

Eu acho que batalhas como herança versus composição são, na verdade, apenas aspectos de uma batalha central mais profunda.

Essa luta é: o que resultará na menor quantidade de código, para maior expansão em melhorias futuras?

É por isso que a pergunta é tão difícil de responder. Em uma língua, pode ser que a herança seja colocada mais rapidamente do que a composição do que em alguma outra língua, ou vice-versa.

Ou pode ser o problema específico que você está resolvendo no código se beneficia mais da conveniência do que de uma visão de longo prazo do crescimento. Isso é tanto um problema de negócios quanto de programação.

Então, para voltar à questão, a herança pode estar errada se você estiver colocando uma capa em algum ponto no futuro - ou se estiver criando código que não precisa estar lá (classes herdadas de outros por não razão, ou pior ainda, as classes base que começam a ter que fazer muitos testes condicionais para crianças).

    
por 10.12.2012 / 07:57
fonte