Pergunta

É um bom conceito de usar herança múltipla ou eu posso fazer outras coisas em vez disso?

Foi útil?

Solução

A herança múltipla (abreviado como MI) cheiros , o que significa que normalmente , ele foi feito por razões ruins, e ela vai explodir de volta na cara do mantenedor.

Resumo

  1. Considere composição dos recursos, em vez de herança
  2. Seja cauteloso com o diamante de Dread
  3. Considere herança de múltiplas interfaces em vez de objetos
  4. Às vezes, múltipla herança é a coisa certa. Se for, então usá-lo.
  5. Esteja preparado para defender sua arquitetura herdada de múltiplos em revisões de código

1. Talvez composição?

Isto é verdade para a herança, e assim, é ainda mais verdadeiro para herança múltipla.

Será que o seu objeto precisa realmente para herdar de outro? A Car não precisa herdar de um Engine ao trabalho, nem de um Wheel. A Car tem uma Engine e quatro Wheel.

Se você usar herança múltipla para resolver esses problemas em vez de composição, então você fez algo errado.

2. O Diamond of Dread

Normalmente, você tem um A classe, então B e C ambos herdam A. E (não me pergunte o porquê) alguém, então decide que D deve herdar tanto do B e C.

Eu encontrei esse tipo de problema duas vezes em 8 oitos anos, e é divertido ver por causa de:

  1. Quanto de um erro que era desde o princípio (Em ambos os casos, D não deve ter herdado de ambos B e C), porque isso era ruim arquitetura (na verdade, C não deveria ter existido ...)
  2. Quanto mantenedores estavam pagando para isso, porque em C ++, o A classe pai estava presente duas vezes em sua D classe neto, e, assim, atualizando um dos pais A::field campo significava ou atualizá-lo duas vezes (através B::field e C::field), ou ter algo ir silenciosamente errado e acidente, mais tarde (um ponteiro de novo no B::field e C::field de exclusão ...)

Usando a palavra-chave virtual em C ++ para qualificar a herança evita o layout duplo descrito acima, se isso não é o que você quer, mas de qualquer maneira, na minha experiência, provavelmente você está fazendo algo errado ...

Na hierarquia de objeto, você deve tentar manter a hierarquia como uma árvore (um nó tem uma mãe), não como um gráfico.

Mais sobre o Diamond (edição 2017/05/03)

O problema real com o Diamond of Dread em C ++ ( assumindo o design é som - ter o seu código revisado !), É que você precisa fazer uma escolha :

  • É desejável para o A classe de existir duas vezes em seu layout, eo que isso significa? Se sim, então por todos os meios herdar de duas vezes.
  • se ele deve existir apenas uma vez, em seguida, herdam-se praticamente.

Esta escolha é inerente ao problema e, em C ++, ao contrário de outras línguas, você pode realmente fazê-lo sem dogma forçando seu projeto a nível da linguagem.

Mas, como todos os poderes, com esse poder vem a responsabilidade:. Ter o seu projeto revisado

3. Interfaces

herança múltipla de zero ou uma classes concretas, e zero ou mais interfaces é geralmente bem, porque você não vai encontrar o diamante de Dread descrito acima. Na verdade, esta é a forma como as coisas são feitas em Java.

Normalmente, o que você quer dizer quando herda C de A e B é que os usuários podem usar C como se fosse um A, e / ou como se fosse um B.

Em C ++, uma interface é uma classe abstrata que tem:

  1. todo o seu método declarado puro virtual (sufixo = 0) (removido o 2017/05/03)
  2. não membvariáveis ??er

A herança múltipla de zero a um objeto real, e zero ou mais interfaces não é considerado "mau cheiro" (pelo menos, não tanto).

Mais sobre o ++ Abstract interface C (edit 2017/05/03)

Em primeiro lugar, o padrão NVI pode ser usado para produzir uma interface, porque os critérios reais é não ter estado (ou seja, há variáveis ??de membro, exceto this). ponto do seu interface abstrata é publicar um contrato ( "você pode me chamar dessa maneira, e desta forma"), nada mais, nada menos. A limitação de ter método virtual só resumo deve ser uma escolha design, não uma obrigação.

Em segundo lugar, em C ++, faz sentido para herdar praticamente de interfaces abstratas, (mesmo com o adicional de custo / indirecta). Se não o fizer, ea herança de interface parece tempo múltipla em sua hierarquia, então você vai ter ambigüidades.

Em terceiro lugar, orientação a objetos é grande, mas não é A única verdade Out There TM em C ++. Use as ferramentas certas, e lembre-se sempre que você tem outros paradigmas em C ++ oferecendo diferentes tipos de soluções.

4. Você realmente precisa de herança múltipla?

Às vezes, sim.

Normalmente, sua classe C está herdando de A e B e A e B são dois objetos não relacionados (ou seja, não na mesma hierarquia, nada em comum, conceitos diferentes, etc.).

Por exemplo, você poderia ter um sistema de Nodes com X, Y, Z coordenadas, capaz de fazer um monte de cálculos geométricos (talvez um ponto, parte de objetos geométricos) e cada nó é um agente automatizado, capaz de se comunicar com outros agentes.

Talvez você já tem acesso a duas bibliotecas, cada um com seu próprio namespace (outra razão para namespaces de uso ... Mas você usar namespaces, não é?), Sendo um deles geo eo outro ser ai

Então você tem o seu próprio own::Node derivar tanto de ai::Agent e geo::Point.

Este é o momento em que você deve se perguntar se você não deve usar composição vez. Se own::Node é realmente tanto um ai::Agent e uma geo::Point, em seguida, a composição não vai fazer.

Em seguida, você vai precisar de herança múltipla, tendo o seu own::Node comunicar com outros agentes de acordo com a sua posição em um espaço 3D.

(Você vai notar que ai::Agent e geo::Point são completamente, totalmente, totalmente NÃO RELACIONADA ... Isso reduz drasticamente o perigo de herança múltipla)

Outros casos (editar 2017/05/03)

Há outros casos:

  • usando herança (espero privada) como detalhe de implementação
  • algumas expressões idiomáticas C ++ como políticas poderia usar herança múltipla (quando cada parte precisa se comunicar com os outros através de this)
  • a herança virtual a partir de std :: exceção ( é a herança virtual necessária para Exceções? )
  • etc.

Às vezes você pode usar a composição, e às vezes MI é melhor. O ponto é: Você tem uma escolha. Fazê-lo de forma responsável (e ter seu código revisado).

5. Então, eu deveria fazer herança múltipla?

Na maioria das vezes, na minha experiência, não. MI não é a ferramenta certa, mesmo que pareça ao trabalho, porque ele pode ser usado pela preguiça de pilha apresenta juntos sem perceber as conseqüências (como fazer um Car tanto um Engine e uma Wheel).

Mas, às vezes, sim. E, nesse momento, nada vai funcionar melhor do MI.

Mas porque MI é mau cheiro, estar preparado para defender sua arquitetura em revisões de código (e defendê-la é uma coisa boa, porque se você não é capaz de defendê-lo, então você não deve fazê-lo).

Outras dicas

A partir de um href="http://www.artima.com/intv/modern.html" entrevista com Bjarne Stroustrup :

As pessoas dizem muito corretamente que você não precisa de herança múltipla, porque qualquer coisa que você pode fazer com a herança múltipla, você também pode fazer com herança única. Você acabou de usar o truque delegação eu mencionei. Além disso, você não precisa de qualquer herança em tudo, porque tudo o que fazemos com herança simples, você também pode fazer sem herança do encaminhamento através de uma classe. Na verdade, você não precisa de quaisquer classes, quer, porque você pode fazer tudo com ponteiros e estruturas de dados. Mas porque você iria querer fazer aquilo? Quando é conveniente utilizar os serviços linguísticos? Quando você prefere uma solução alternativa? Já vi casos em que a herança múltipla é útil, e eu mesmo vi casos em que bastante complicada herança múltipla é útil. Geralmente, eu prefiro usar as facilidades oferecidas pela linguagem para fazer soluções

Não há nenhuma razão para evitá-lo e ele pode ser muito útil em situações. Você precisa estar ciente dos potenciais problemas embora.

O maior deles sendo o diamante da morte:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

Você agora tem duas "cópias" de avô dentro de crianças.

C ++ tenha pensado nisso embora e permite que você faça inheritence virtual para contornar os problemas.

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

Sempre revise seu projeto, garantir que você não está usando herança para economizar em reutilização de dados. Se você pode representar a mesma coisa com composição (e, normalmente, você pode) esta é uma abordagem muito melhor.

Veja w:. Herança múltipla

A herança múltipla recebeu crítica e, como tal, não é implementado em muitas línguas. Críticas inclui:

  • Maior complexidade
  • ambigüidade semântica, muitas vezes resumido como o diamante problema .
  • Não sendo capaz de explicitamente herdar várias vezes de um único classe
  • Ordem de mudança de herança semântica de classe.

A herança múltipla em línguas com construtores de estilo C ++ / Java agrava o problema herança de construtores e encadeamento construtor, criando assim a manutenção e problemas de extensibilidade nestes línguas. Objetos na herança relações com variando grandemente métodos de construção são difíceis de implementar sob o construtor encadeamento paradigma.

forma moderna de resolver isso para uso de interface (classe abstrata pura) como COM e Java interface.

Eu posso fazer outras coisas no lugar deste?

Sim, você pode. Vou roubar GoF .

  • Programa a uma interface, não uma implementação
  • Prefere composição sobre herança

herança público é uma relação IS-A, e às vezes uma classe será um tipo de várias classes diferentes, e às vezes é importante para refletir isso.

"Mixins" também às vezes são úteis. Eles são geralmente pequenas classes, geralmente não herdam nada, fornecendo funcionalidades úteis.

Enquanto a hierarquia de herança é bastante rasa (como deveria ser quase sempre), e bem gerido, você é improvável que se obtenha a herança diamante temido. O diamante não é um problema com todas as línguas que usam a herança múltipla, mas o tratamento dele "s C ++ é frequentemente difícil e, por vezes, intrigante.

Enquanto eu correr em casos em que a herança múltipla é muito útil, eles são realmente bastante raro. Isto é provável porque eu prefiro usar outros métodos de projeto quando eu realmente não precisa de herança múltipla. Eu prefiro evitar confundir construções de linguagem, e é fácil de casos de herança construto onde você tem que ler o manual muito bem para descobrir o que está acontecendo.

Você não deve "evitar" herança múltipla, mas você deve estar ciente dos problemas que podem surgir, tais como o 'problema diamante' ( http://en.wikipedia.org/wiki/Diamond_problem ) e tratar o poder dado a você com cuidado, como você deve com todos os poderes.

Cada linguagem de programação tem um tratamento um pouco diferente da programação orientada a objeto com prós e contras. A versão de C ++ coloca a ênfase diretamente no desempenho e tem a desvantagem que acompanha que é perturbadoramente fácil escrever código inválido - e isso é verdade de herança múltipla. Como consequência, há uma tendência para os programadores abster-se de este recurso.

Outras pessoas têm abordado a questão do que a herança múltipla não é bom para. Mas temos visto muito poucos comentários que mais ou menos implica que a razão para evitá-lo é porque ele não é seguro. Bem, sim e não.

Como é frequentemente verdadeiro em C ++, se você seguir uma diretriz básica que você pode usá-lo com segurança, sem ter que "olhar por cima do ombro" constantemente. A idéia fundamental é que você distinguir um tipo especial de definição de classe chamado de um "mix-in"; classe é uma mistura-se em todas as suas funções de membro são virtual (ou puro virtual). Então você tem permissão para herdar de uma única classe principal e como muitos "mixins" como você gosta - mas você deve herdar mixins com a palavra-chave "virtual". por exemplo.

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

A minha sugestão é que se você pretende usar uma classe como um mix-in class você também adotar uma convenção de nomenclatura para tornar mais fácil para qualquer um ler o código para ver o que está acontecendo e para verificar que você está jogando pelas regras do a diretriz básica. E você vai achar que funciona muito melhor se o seu mix-ins têm construtores padrão também, apenas por causa da forma virtual classes de base de trabalho. E lembre-se de fazer todos os destruidores virtuais também.

Note que o meu uso da palavra "mix-in" aqui não é o mesmo que a classe de modelo parametrizado (veja este link para uma boa explicação) mas eu acho que é um uso justo da terminologia.

Agora eu não quero dar a impressão de que esta é a única maneira de usar herança múltipla com segurança. É apenas uma maneira que é bastante fácil de verificar.

Com o risco de obter um resumo pouco, acho que é esclarecedor pensar em herança dentro do quadro da teoria das categorias.

Se pensarmos em todas as nossas aulas e flechas entre eles denotando relações de herança, então algo como isto

A --> B

significa que deriva class B de class A. Note-se que, dada

A --> B, B --> C

dizemos C deriva B, que deriva de um, então C é dito também derivar de A, assim

A --> C

Além disso, podemos dizer que, para cada A classe que deriva trivialmente A de A, assim, o nosso modelo de herança cumpre a definição de uma categoria. Em linguagem mais tradicional, temos um Class categoria com objetos todas as classes e morphisms as relações de herança.

Isso é um pouco de configuração, mas com isso vamos dar uma olhada no nosso diamante of Doom:

C --> D
^     ^
|     |
A --> B

É um diagrama de vista obscuro, mas ele vai fazer. herda Então D de todas A, B e C. Além disso, e ficando mais perto de enfrentar a pergunta do OP, D também herda a partir de qualquer superclasse de A. Podemos desenhar um diagrama

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

Agora, os problemas associados com o diamante da Morte aqui estão quando C e compartilhar B alguma propriedade / método nomes e as coisas ficam ambígua; No entanto, se mover qualquer comportamento compartilhado em A então a ambigüidade desaparece.

Coloque em termos categóricos, queremos A, B e C ser tal que se B e C Herdar do Q então A pode ser reescrita como como subclasse de Q. Isso faz com que A algo chamado pushout .

Há também uma construção simétrica em D chamado pullback . Esta é essencialmente a classe útil geral mais você pode construir que herda de ambos B e C. Ou seja, se você tiver qualquer outra R classe multiplicam herdando de B e C, então D é uma classe onde R pode ser reescrita como como subclasse de D.

Certificar-se de suas pontas do diamante são pullbacks e pushouts nos dá uma boa maneira de questões genericamente nome-colidindo punho ou de manutenção que possa ocorrer o contrário.

Nota Paercebal 's resposta inspirou este como suas admoestações são implícito no modelo acima, dado que trabalhamos na categoria de Classe de todas as classes possíveis.

Eu queria generalizar seu argumento a algo que mostra como é complicado múltiplas relações de herança pode ser tanto poderoso e não-problemático.

TL; DR Pense nas relações de herança em seu programa como constituindo uma categoria. Então você pode evitar Diamante de problemas desgraça, fazendo pushouts aulas multiplicam-herdadas e simetricamente, fazendo uma classe pai comum, que é um pullback.

Nós usamos Eiffel. Temos excelentes MI. Não se preocupe. Sem problemas. Facilmente gerenciadas. Há momentos para não usar MI. No entanto, é mais útil do que as pessoas percebem porque eles são: A) em uma linguagem perigosa que não administrá-lo bem -OU- B) satisfeito com a forma como eles trabalharam em torno de MI por anos e anos -OU- C) outras razões ( muito numerosas para listar estou certo -. ver respostas acima)

Para nós, usando Eiffel, MI é tão natural quanto qualquer outra coisa e outra ferramenta fina na caixa de ferramentas. Francamente, nós estamos bastante despreocupada que ninguém mais está usando Eiffel. Não se preocupe. Estamos felizes com o que temos e convidá-lo para dar uma olhada.

Enquanto você está procurando: Tome nota especial de vácuo para a segurança ea erradicação da desreferenciação de ponteiro NULO. Enquanto estamos todos dançando em torno de MI, os ponteiros estão se perdendo! : -)

Você deve usá-lo com cuidado, existem alguns casos, como o Diamante Problema , quando As coisas podem dar complicado.

text alt
(fonte: learncpp.com )

usa e abusa da herança.

O artigo faz um ótimo trabalho de explicar a herança, e é perigos.

Além do padrão de diamante, herança múltipla tende a tornar o modelo de objeto mais difícil de entender, o que aumenta os custos de manutenção.

A composição é intrinsecamente fácil de entender, compreender e explicar. Ele pode se tornar tedioso para escrever código, mas uma boa IDE (que tem sido alguns anos desde que eu tenho trabalhado com Visual Studio, mas certamente os IDEs Java todos têm grandes ferramentas de composição de atalho Automatizando) deverá fazê-lo durante esse obstáculo.

Além disso, em termos de manutenção, o "problema diamante" surge em casos de herança não-literais também. Por exemplo, se você tem A e B e a classe C se estende a ambos, e A tem um método 'makeJuice' que faz o sumo de laranja e você estender isso para fazer suco de laranja com uma torção do cal: o que acontece quando o designer de ' B' adiciona um 'método makeJuice' que gera uma corrente elétrica? 'A' e 'B' pode ser "pais" compatíveis agora , mas isso não significa que eles vão ser sempre assim!

No geral, a máxima de que tende a evitar a herança e herança especialmente múltipla, é o som. Como todas as máximas, há exceções, mas você precisa ter certeza de que há um piscar verde apontando sinal de néon em todas as exceções que você código (e treinar seu cérebro para que a qualquer momento você vê essas árvores de herança você desenha em sua própria piscar de néon verde sinal), e que você verifique se tudo faz sentido, de vez em quando.

A questão-chave com MI de objetos concretos é que raramente você tem um objeto que legitimamente deve "Seja um A e ser um B", por isso raramente é a solução correta por razões lógicas. Muito mais frequentemente, você tem um objeto C que obedece "C pode atuar como um A ou um B", que você pode alcançar através da interface herança e composição. Mas não herança mistake- de múltiplas interfaces ainda é MI, apenas um subconjunto do mesmo.

Para C ++ em particular, a fraqueza fundamental do recurso não é a existência real de herança múltipla, mas algumas construções que permite que quase sempre são mal formados. Por exemplo, herdar várias cópias do mesmo objeto como:

class B : public A, public A {};

é mal formado por definição. Traduzida em Inglês este é "B é um A e um A". Assim, mesmo em linguagem humana há uma ambigüidade grave. Você quis dizer "B tem 2 como" ou apenas "B é um A" ?. Permitindo que tal código patológico, e pior tornando-se um exemplo de uso, fez C ++ nenhum favor quando ele veio para fazer um caso para manter o recurso em línguas sucessoras.

Você pode usar a composição de preferência a herança.

O sentimento geral é que a composição é melhor, e é muito bem discutido.

que leva 4/8 bytes por turma envolvidos. (Uma este ponteiro por turma).

Esta nunca poderia ser uma preocupação, mas se um dia você tem uma estrutura de dados micro que é instanciado bilhões de tempo que vai ser.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top