Respostas:
A herança múltipla (abreviada como MI) cheira , o que significa que , geralmente , isso foi feito por motivos ruins e será revertido para o mantenedor.
Isso é verdadeiro para herança e, portanto, é ainda mais verdadeiro para herança múltipla.
Seu objeto realmente precisa herdar de outro? A Car
não precisa herdar de um Engine
para trabalhar, nem de um Wheel
. A Car
tem um Engine
e quatro Wheel
.
Se você usa herança múltipla para resolver esses problemas, em vez de composição, você fez algo errado.
Normalmente, você tem uma classe A
, em seguida, B
e C
ambos herdam A
. E (não me pergunte por que) alguém decide que D
deve herdar ambos de B
e C
.
Eu encontrei esse tipo de problema duas vezes em oito oito anos e é divertido ver isso por causa de:
D
não deveria ter herdado de ambos B
e C
), porque essa era uma arquitetura ruim (de fato, C
não deveria ter existido ...)A
estava presente duas vezes na classe neto D
e, portanto, atualizar um campo pai A::field
significava atualizá-lo duas vezes (através B::field
e C::field
) ou ter algo errado e travar silenciosamente mais tarde (insira um ponteiro B::field
e exclua C::field
...)Usar a palavra-chave virtual em C ++ para qualificar a herança evita o layout duplo descrito acima, se não é isso que você deseja, mas, de qualquer forma, na minha experiência, você provavelmente está fazendo algo errado ...
Na hierarquia de objetos, você deve tentar manter a hierarquia como uma árvore (um nó tem UM pai), não como um gráfico.
O verdadeiro problema com o Diamond of Dread em C ++ ( assumindo que o design seja sólido - revise seu código! ), É que você precisa fazer uma escolha :
A
exista duas vezes no seu layout e o que isso significa? Se sim, então, por todos os meios, herda duas vezes.Essa opção é inerente ao problema e, em C ++, ao contrário de outras linguagens, você pode realmente fazê-lo sem dogmas forçando seu design no nível da linguagem.
Mas, como todos os poderes, com esse poder vem a responsabilidade: revise seu projeto.
Herança múltipla de zero ou uma classe concreta e zero ou mais interfaces geralmente está OK, porque você não encontrará o Diamond of Dread descrito acima. De fato, é assim que as coisas são feitas em Java.
Normalmente, o que você quer dizer quando C herda A
e B
é que os usuários podem usar C
como se fosse um A
e / ou como se fosse um B
.
No C ++, uma interface é uma classe abstrata que possui:
A herança múltipla de zero a um objeto real e zero ou mais interfaces não são consideradas "fedorentas" (pelo menos, não tanto).
Primeiro, o padrão NVI pode ser usado para produzir uma interface, porque o critério real é não ter estado (ou seja, nenhuma variável membro, exceto this
). O objetivo da sua interface abstrata é publicar um contrato ("você pode me chamar assim e assim"), nada mais, nada menos. A limitação de ter apenas o método virtual abstrato deve ser uma opção de design, não uma obrigação.
Segundo, em C ++, faz sentido herdar virtualmente de interfaces abstratas (mesmo com o custo / indireção adicional). Caso contrário, e a herança da interface aparece várias vezes na sua hierarquia, você terá ambiguidades.
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 de que existem outros paradigmas em C ++ que oferecem diferentes tipos de soluções.
Às vezes sim.
Normalmente, a sua C
classe é herdada de A
e B
e A
e B
são dois objetos independentes (ou seja, não na mesma hierarquia, nada em comum, conceitos diferentes, etc.).
Por exemplo, você poderia ter um sistema Nodes
com coordenadas X, Y, Z, capaz de fazer muitos 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á tenha acesso a duas bibliotecas, cada uma com seu próprio espaço para nome (outra razão para usar espaços para nome ... Mas você usa espaço para nome, não é?), Um sendo geo
e outro sendoai
Então você tem sua própria own::Node
derivada de ai::Agent
e geo::Point
.
Este é o momento em que você deve se perguntar se não deve usar a composição. Se own::Node
realmente é realmente a ai::Agent
e a geo::Point
, a composição não funciona.
Você precisará de herança múltipla, own::Node
comunicando-se com outros agentes de acordo com a posição deles em um espaço 3D.
(Você notará isso ai::Agent
e geo::Point
é completamente, totalmente, totalmente NÃO RETIRADO ... Isso reduz drasticamente o risco de herança múltipla)
Existem outros casos:
this
)Às vezes você pode usar a composição, e às vezes o MI é melhor. O ponto é: você tem uma escolha. Faça com responsabilidade (e revise seu código).
Na maioria das vezes, na minha experiência, não. O MI não é a ferramenta certa, mesmo que pareça funcionar, porque pode ser usada pelo preguiçoso para agrupar recursos sem perceber as consequências (como criar um Car
tanto um Engine
quanto um Wheel
).
Mas as vezes sim. E nesse momento, nada funcionará melhor que o MI.
Mas como o MI é fedorento, esteja preparado para defender sua arquitetura nas revisões de código (e defendê-lo é uma coisa boa, porque se você não conseguir defendê-lo, não deverá fazê-lo).
De uma entrevista com Bjarne Stroustrup :
As pessoas dizem corretamente que você não precisa de herança múltipla, porque qualquer coisa que você possa fazer com herança múltipla também pode fazer com herança única. Você acabou de usar o truque de delegação que mencionei. Além disso, você não precisa de nenhuma herança, porque tudo o que você faz com uma única herança também pode fazer sem herança, encaminhando através de uma classe. Na verdade, você também não precisa de nenhuma classe, porque pode fazer tudo isso com ponteiros e estruturas de dados. Mas porque você iria querer fazer aquilo? Quando é conveniente usar as instalações de idiomas? Quando você prefere uma solução alternativa? Já vi casos em que a herança múltipla é útil e até casos em que a herança múltipla bastante complicada é útil. Geralmente, eu prefiro usar os recursos oferecidos pelo idioma para solucionar soluções
const
- eu tive que escrever soluções alternativas desajeitadas (geralmente usando interfaces e composição) quando uma classe realmente precisava ter variantes mutáveis e imutáveis. No entanto, nunca perdi uma herança múltipla e nunca senti que precisei escrever uma solução alternativa devido à falta desse recurso. Essa é a diferença. Em todos os casos que já vi, não usar o MI é a melhor opção de design, não uma solução alternativa.
Não há razão para evitá-lo e pode ser muito útil em situações. Você precisa estar ciente dos possíveis problemas.
O maior deles é o diamante da morte:
class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;
Agora você tem duas "cópias" do GrandParent no Child.
O C ++ pensou nisso e permite que você faça uma herança 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 design, verifique se você não está usando herança para economizar na reutilização de dados. Se você pode representar a mesma coisa com a composição (e normalmente você pode), essa é uma abordagem muito melhor.
GrandParent
em Child
. Existe um medo do IM, porque as pessoas simplesmente pensam que podem não entender as regras de linguagem. Mas quem não pode obter essas regras simples também não pode escrever um programa não trivial.
Veja w: Herança Múltipla .
A herança múltipla recebeu críticas e, como tal, não é implementada em muitos idiomas. As críticas incluem:
- Maior complexidade
- A ambiguidade semântica frequentemente é resumida como o problema do diamante .
- Não poder herdar explicitamente várias vezes de uma única classe
- Ordem da herança alterando a semântica da classe.
A herança múltipla em linguagens com construtores de estilo C ++ / Java exacerba o problema de herança de construtores e encadeamento de construtores, criando problemas de manutenção e extensibilidade nessas linguagens. Objetos em relacionamentos de herança com métodos de construção bastante variados são difíceis de implementar sob o paradigma de encadeamento do construtor.
Maneira moderna de resolver isso para usar a interface (classe abstrata pura) como a interface COM e Java.
Eu posso fazer outras coisas no lugar disso?
Sim você pode. Eu vou roubar do GoF .
A herança pública é um relacionamento IS-A e, às vezes, uma classe é um tipo de várias classes diferentes, e às vezes é importante refletir isso.
"Mixins" também são algumas vezes úteis. Geralmente são classes pequenas, geralmente não herdadas de nada, fornecendo funcionalidade útil.
Desde que a hierarquia de herança seja razoavelmente superficial (como quase sempre deve ser) e bem gerenciada, é improvável que você tenha a temida herança de diamante. O diamante não é um problema em todos os idiomas que usam herança múltipla, mas o tratamento feito pelo C ++ é freqüentemente estranho e às vezes intrigante.
Embora eu tenha encontrado casos em que a herança múltipla é muito útil, eles são realmente bastante raros. Provavelmente, porque eu prefiro usar outros métodos de design quando realmente não preciso de herança múltipla. Prefiro evitar construções de linguagem confusas, e é fácil construir casos de herança em que é necessário ler muito bem o manual para descobrir o que está acontecendo.
Você não deve "evitar" a herança múltipla, mas deve estar ciente de problemas que possam surgir, como o 'problema dos diamantes' ( http://en.wikipedia.org/wiki/Diamond_problem ) e tratar com cuidado o poder que lhe é dado , como deveria com todos os poderes.
Correndo o risco de ficar um pouco abstrato, acho esclarecedor pensar em herança dentro do quadro da teoria das categorias.
Se pensarmos em todas as nossas classes e flechas entre elas, denotando relações de herança, algo assim
A --> B
significa que class B
deriva de class A
. Observe que, dado
A --> B, B --> C
dizemos que C deriva de B, que deriva de A, então também se diz que C deriva de A, assim
A --> C
Além disso, dizemos que, para toda classe A
que A
deriva trivialmente A
, nosso modelo de herança cumpre a definição de categoria. Em linguagem mais tradicional, temos uma categoria Class
com objetos de todas as classes e morfismos nas relações de herança.
Isso é um pouco de configuração, mas com isso vamos dar uma olhada no nosso Diamond of Doom:
C --> D
^ ^
| |
A --> B
É um diagrama sombrio, mas serve. Então D
herda de todos A
, B
e C
. Além disso, e chegando mais perto de abordar a questão do OP, D
também herda de qualquer superclasse de A
. Podemos desenhar um diagrama
C --> D --> R
^ ^
| |
A --> B
^
|
Q
Agora, os problemas associados ao Diamante da Morte aqui são quando C
e B
compartilham alguns nomes de propriedades / métodos e as coisas ficam ambíguas; no entanto, se passarmos para um comportamento compartilhado A
, a ambiguidade desaparecerá.
Coloque em termos categóricos, queremos A
, B
e C
seja tal que se B
e C
herdar a partir de Q
então A
possam ser reescritos como subclasse de Q
. Isso faz A
algo chamado pushout .
Há também uma construção simétrica D
chamada de recuo . Esta é essencialmente a classe útil mais geral que você pode construir, que herda de ambos B
e C
. Ou seja, se você tiver outra classe R
herdada multiplicada de B
e C
, então D
é uma classe em que R
pode ser reescrita como subclasse de D
.
Garantir que as pontas do diamante sejam recuos e empurrões nos oferece uma boa maneira de lidar genericamente com problemas de conflito ou manutenção de nomes que possam surgir de outra forma.
Nota A resposta de Paercebal inspirou isso, pois suas advertências estão implícitas no modelo acima, uma vez que trabalhamos na categoria completa Classe de todas as classes possíveis.
Eu queria generalizar seu argumento para algo que mostra como os relacionamentos de herança múltipla complicados podem ser poderosos e não problemáticos.
TL; DR Pense nos relacionamentos de herança em seu programa como formando uma categoria. Para evitar problemas no Diamond of Doom, faça pushouts de classes com herança múltipla e simetricamente, criando uma classe pai comum que é um retrocesso.
Nós usamos Eiffel. Temos um excelente MI. Não se preocupe. Sem problemas. Facilmente gerenciado. Há momentos para NÃO usar o MI. No entanto, é mais útil do que as pessoas percebem porque são: A) em uma linguagem perigosa que não a administra bem -OR- B) satisfeita com a maneira como elas trabalham com o MI há anos e anos -OR- C) outros motivos ( muito numeroso para listar, tenho certeza - veja as respostas acima).
Para nós, o uso do Eiffel, o MI é tão natural quanto qualquer outra coisa e outra boa ferramenta na caixa de ferramentas. Francamente, não estamos preocupados com o fato de ninguém mais estar usando Eiffel. Não se preocupe. Estamos felizes com o que temos e convidamos você a dar uma olhada.
Enquanto você estiver procurando: Observe especialmente a segurança do vácuo e a erradicação da desreferenciação de ponteiro nulo. Enquanto todos dançamos ao redor do MI, seus indicadores estão se perdendo! :-)
Toda linguagem de programação tem um tratamento ligeiramente diferente da programação orientada a objetos com prós e contras. A versão do C ++ enfatiza diretamente o desempenho e tem a desvantagem de que é perturbadoramente fácil escrever código inválido - e isso é verdade para a herança múltipla. Como conseqüência, há uma tendência de afastar os programadores desse recurso.
Outras pessoas abordaram a questão de que herança múltipla não serve. Mas vimos alguns comentários que implicam mais ou menos que o motivo para evitá-lo é porque não é seguro. Bem, sim e não.
Como costuma acontecer no C ++, se você seguir uma diretriz básica, poderá usá-la com segurança sem precisar "olhar por cima do ombro" constantemente. A idéia principal é distinguir um tipo especial de definição de classe chamada "mix-in"; A classe é um mix-in se todas as suas funções de membro forem virtuais (ou virtuais puras). Você poderá herdar de uma única classe principal e quantos "mix-ins" quiser - 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 { ..... };
Minha sugestão é que, se você pretende usar uma classe como uma classe combinada, também adota uma convenção de nomenclatura para facilitar a qualquer pessoa que estiver lendo o código ver o que está acontecendo e verificar se está seguindo as regras da diretriz básica. . E você descobrirá que funciona muito melhor se os seus mix-ins também tiverem construtores padrão, apenas por causa da maneira como as classes base virtuais funcionam. E lembre-se de tornar todos os destruidores virtuais também.
Observe que meu uso da palavra "mix-in" aqui não é o mesmo que a classe de modelo parametrizada (consulte este link para uma boa explicação), mas acho que é um uso justo da terminologia.
Agora, não quero dar a impressão de que essa é a única maneira de usar a herança múltipla com segurança. É apenas uma maneira fácil de verificar.
Você deve usá-lo com cuidado, existem alguns casos, como o problema do diamante , em que as coisas podem complicar-se.
(fonte: learncpp.com )
Printer
nem deveria ser a PoweredDevice
. A Printer
é para impressão, não para gerenciamento de energia. A implementação de uma impressora específica pode ter que fazer algum gerenciamento de energia, mas esses comandos de gerenciamento de energia não devem ser expostos diretamente aos usuários da impressora. Não consigo imaginar nenhum uso real dessa hierarquia no mundo real.
O artigo faz um ótimo trabalho ao explicar a herança e seus perigos.
Além do padrão de diamante, a herança múltipla tende a dificultar a compreensão do modelo de objeto, o que, por sua vez, aumenta os custos de manutenção.
A composição é intrinsecamente fácil de entender, compreender e explicar. Pode ser entediante escrever código, mas um bom IDE (já faz alguns anos desde que trabalhei com o Visual Studio, mas certamente os Java IDEs têm ótimas ferramentas de automação de atalhos de composição) deve superar esse obstáculo.
Além disso, em termos de manutenção, o "problema do diamante" surge também em instâncias de herança não literais. Por exemplo, se você tem A e B e sua classe C os estende, e A tem o método 'makeJuice' que produz suco de laranja e você o extende para fazer suco de laranja com um toque de limão: o que acontece quando o designer de ' B 'adiciona um método' makeJuice 'que gera corrente elétrica? 'A' e 'B' podem ser "pais" compatíveis no momento , mas isso não significa que sempre serão assim!
No geral, a máxima tendência para evitar a herança, e principalmente a herança múltipla, é sólida. Como todas as máximas, há exceções, mas você precisa garantir que haja um sinal de néon verde piscando apontando para todas as exceções que você codificar (e treinar seu cérebro para que, sempre que vir essas árvores de herança, você desenhe em seu próprio néon verde piscante) ) e verifique se tudo faz sentido de vez em quando.
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
Uhhh, você obtém um erro de compilação, é claro (se o uso for ambíguo).
O principal problema com o MI de objetos concretos é que raramente você tem um objeto que legitimamente deveria "Ser um A e ser um B", portanto, raramente é a solução correta por motivos lógicos. Com muito mais frequência, você tem um objeto C que obedece "C pode atuar como um A ou um B", que pode ser alcançado por herança e composição da interface. Mas não se engane - a herança de várias interfaces ainda é MI, apenas um subconjunto dela.
Para C ++, em particular, a principal fraqueza do recurso não é a EXISTÊNCIA real de herança múltipla, mas algumas construções que ele permite quase sempre estão malformadas. Por exemplo, herdando várias cópias do mesmo objeto, como:
class B : public A, public A {};
está mal formado POR DEFINIÇÃO. Traduzido para o inglês, este é "B é um A e um A". Então, mesmo na linguagem humana, há uma ambiguidade severa. Você quis dizer "B tem 2 Como" ou apenas "B é um A" ?. Permitir esse código patológico e, pior ainda, torná-lo um exemplo de uso, não favoreceu o C ++ quando se tratava de manter o recurso em idiomas sucessores.
Você pode usar a composição em preferência à herança.
O sentimento geral é que a composição é melhor e é muito bem discutida.
The general feeling is that composition is better, and it's very well discussed.
Isso não significa que a composição seja melhor.
são necessários 4/8 bytes por classe envolvida. (Um ponteiro por classe).
Isso pode nunca ser uma preocupação, mas se um dia você tiver uma estrutura de microdados instanciada bilhões de vezes, será.