Est-ce un bon concept d'utiliser l'héritage multiple ou puis-je faire d'autres choses à la place?
Est-ce un bon concept d'utiliser l'héritage multiple ou puis-je faire d'autres choses à la place?
L'héritage multiple (en abrégé MI) sent , ce qui signifie que généralement , il a été fait pour de mauvaises raisons, et il va se retourner contre le responsable.
Cela est vrai pour l'héritage, et donc, c'est encore plus vrai pour l'héritage multiple.
Votre objet a-t-il vraiment besoin d'hériter d'un autre? A Car
n'a pas besoin d'hériter d'un Engine
to work, ni d'un Wheel
. A Car
a un Engine
et quatre Wheel
.
Si vous utilisez l'héritage multiple pour résoudre ces problèmes au lieu de la composition, c'est que vous avez fait quelque chose de mal.
Habituellement, vous avez une classe A
, alors B
et les C
deux héritent de A
. Et (ne me demandez pas pourquoi) quelqu'un décide alors que D
doit hériter à la fois de B
et C
.
J'ai rencontré ce genre de problème deux fois en 8 huit ans, et c'est amusant à voir à cause de:
D
n'aurait pas dû hériter des deux B
et C
), car c'était une mauvaise architecture (en fait, elle C
n'aurait pas dû exister du tout ...)A
était présente deux fois dans sa classe de petit-enfant D
, et donc, mettre à jour un champ parent A::field
signifiait soit le mettre à jour deux fois (via B::field
et C::field
), soit faire en sorte que quelque chose se passe silencieusement et se bloque plus tard (nouveau un pointeur dans B::field
, et supprimer C::field
...)Utiliser le mot-clé virtual en C ++ pour qualifier l'héritage évite la double mise en page décrite ci-dessus si ce n'est pas ce que vous voulez, mais de toute façon, d'après mon expérience, vous faites probablement quelque chose de mal ...
Dans la hiérarchie d'objets, vous devez essayer de conserver la hiérarchie sous forme d'arbre (un nœud a UN parent), et non sous forme de graphique.
Le vrai problème avec le Diamond of Dread en C ++ (en supposant que la conception soit saine - faites réviser votre code! ), C'est que vous devez faire un choix :
A
existe deux fois dans votre mise en page et qu'est-ce que cela signifie? Si oui, alors héritez-en deux fois.Ce choix est inhérent au problème, et en C ++, contrairement à d'autres langages, vous pouvez réellement le faire sans que le dogme ne force votre conception au niveau du langage.
Mais comme tous les pouvoirs, ce pouvoir s'accompagne de responsabilités: faites réviser votre conception.
L'héritage multiple de zéro ou une classe concrète et de zéro ou plusieurs interfaces est généralement correct, car vous ne rencontrerez pas le Diamond of Dread décrit ci-dessus. En fait, c'est ainsi que les choses se font en Java.
Habituellement, ce que vous voulez dire quand C hérite de A
et B
est que les utilisateurs peuvent utiliser C
comme s'il s'agissait d'un A
, et / ou comme s'il s'agissait d'un fichier B
.
En C ++, une interface est une classe abstraite qui a:
L'héritage multiple de zéro à un objet réel, et zéro ou plusieurs interfaces n'est pas considéré comme "malodorant" (du moins, pas autant).
Premièrement, le modèle NVI peut être utilisé pour produire une interface, car le vrai critère est de ne pas avoir d'état (c'est-à-dire pas de variables membres, sauf this
). Le but de votre interface abstraite est de publier un contrat ("vous pouvez m'appeler ainsi, et ainsi"), ni plus, ni moins. La limitation de n'avoir qu'une méthode virtuelle abstraite devrait être un choix de conception, pas une obligation.
Deuxièmement, en C ++, il est logique d'hériter virtuellement d'interfaces abstraites (même avec le coût supplémentaire / l'indirection). Si vous ne le faites pas et que l'héritage d'interface apparaît plusieurs fois dans votre hiérarchie, vous aurez des ambiguïtés.
Troisièmement, l'orientation objet est excellente, mais ce n'est pas The Only Truth Out There TM en C ++. Utilisez les bons outils et souvenez-vous toujours que vous avez d'autres paradigmes en C ++ offrant différents types de solutions.
Parfois oui.
Habituellement, votre C
classe hérite de A
et B
, et A
et B
sont deux objets indépendants (c'est-à-dire pas dans la même hiérarchie, rien en commun, des concepts différents, etc.).
Par exemple, vous pourriez avoir un système Nodes
avec des coordonnées X, Y, Z, capable de faire beaucoup de calculs géométriques (peut-être un point, une partie d'objets géométriques) et chaque nœud est un agent automatisé, capable de communiquer avec d'autres agents.
Peut-être avez-vous déjà accès à deux bibliothèques, chacune avec son propre espace de noms (une autre raison d'utiliser des espaces de noms ... Mais vous utilisez des espaces de noms, n'est-ce pas?), L'une étant geo
et l'autre étantai
Vous avez donc votre propre own::Node
dérivé à la fois de ai::Agent
et geo::Point
.
C'est le moment où vous devriez vous demander si vous ne devriez pas utiliser la composition à la place. Si own::Node
c'est vraiment à la fois un ai::Agent
et un geo::Point
, alors la composition ne fera pas l'affaire.
Ensuite, vous aurez besoin d'héritage multiple, en vous own::Node
permettant de communiquer avec d'autres agents en fonction de leur position dans un espace 3D.
(Vous noterez que ai::Agent
et geo::Point
sont complètement, totalement, totalement SANS LIEN ... Cela réduit considérablement le danger d'héritage multiple)
Il existe d'autres cas:
this
)Parfois, vous pouvez utiliser la composition, et parfois MI est meilleur. Le fait est que vous avez le choix. Faites-le de manière responsable (et faites réviser votre code).
La plupart du temps, d'après mon expérience, non. MI n'est pas le bon outil, même s'il semble fonctionner, car il peut être utilisé par les paresseux pour empiler des fonctionnalités sans se rendre compte des conséquences (comme créer à la Car
fois un an Engine
et un a Wheel
).
Mais parfois, oui. Et à ce moment-là, rien ne fonctionnera mieux que MI.
Mais comme MI a une odeur, soyez prêt à défendre votre architecture dans les revues de code (et la défendre est une bonne chose, car si vous n'êtes pas en mesure de la défendre, vous ne devriez pas le faire).
Extrait d'un entretien avec Bjarne Stroustrup :
la source
const
- j'ai dû écrire des solutions de contournement maladroites (en utilisant généralement des interfaces et une composition) lorsqu'une classe avait vraiment besoin d'avoir des variantes mutables et immuables. Cependant, je n'ai jamais manqué une seule fois l' héritage multiple et je n'ai jamais senti que je devais écrire une solution de contournement en raison de l'absence de cette fonctionnalité. Voilà la différence. Dans tous les cas que j'ai jamais vu, ne pas utiliser MI est le meilleur choix de conception, pas une solution de contournement.Il n'y a aucune raison de l'éviter et cela peut être très utile dans certaines situations. Vous devez cependant être conscient des problèmes potentiels.
Le plus gros étant le diamant de la mort:
Vous avez maintenant deux "copies" de GrandParent dans Child.
C ++ a pensé à cela et vous permet de faire de l'héritage virtuel pour contourner les problèmes.
Vérifiez toujours votre conception, assurez-vous que vous n'utilisez pas l'héritage pour économiser sur la réutilisation des données. Si vous pouvez représenter la même chose avec la composition (et généralement vous le pouvez), c'est une bien meilleure approche.
la source
GrandParent
dansChild
. Il y a une peur de l'IM, car les gens pensent simplement qu'ils pourraient ne pas comprendre les règles linguistiques. Mais quiconque ne peut pas obtenir ces règles simples ne peut pas non plus écrire un programme non trivial.Voir w: héritage multiple .
Moyen moderne de résoudre ce problème en utilisant une interface (pure classe abstraite) comme l'interface COM et Java.
Oui, vous pouvez. Je vais voler le GoF .
la source
L'héritage public est une relation IS-A, et parfois une classe sera un type de plusieurs classes différentes, et parfois il est important de refléter cela.
Les "mixins" sont également parfois utiles. Ce sont généralement de petites classes, n'héritant généralement de rien, fournissant des fonctionnalités utiles.
Tant que la hiérarchie d'héritage est assez superficielle (comme elle devrait presque toujours l'être) et bien gérée, il est peu probable que vous obteniez l'héritage redouté de diamants. Le diamant n'est pas un problème avec tous les langages qui utilisent l'héritage multiple, mais son traitement par C ++ est souvent maladroit et parfois déroutant.
Bien que j'aie rencontré des cas où l'héritage multiple est très pratique, ils sont en fait assez rares. C'est probablement parce que je préfère utiliser d'autres méthodes de conception lorsque je n'ai pas vraiment besoin d'héritage multiple. Je préfère éviter les constructions de langage confuses, et il est facile de construire des cas d'héritage où vous devez très bien lire le manuel pour comprendre ce qui se passe.
la source
Vous ne devez pas "éviter" l'héritage multiple, mais vous devez être conscient des problèmes qui peuvent survenir tels que le "problème du diamant" ( http://en.wikipedia.org/wiki/Diamond_problem ) et traiter le pouvoir qui vous est donné avec soin , comme vous devriez avec tous les pouvoirs.
la source
Au risque de devenir un peu abstrait, je trouve éclairant de penser l'héritage dans le cadre de la théorie des catégories.
Si nous pensons à toutes nos classes et aux flèches entre elles indiquant des relations d'héritage, alors quelque chose comme ça
signifie que
class B
dérive declass A
. Notez que, étant donnéon dit que C dérive de B qui dérive de A, donc on dit aussi que C dérive de A, donc
De plus, nous disons que pour chaque classe
A
dontA
dérive trivialementA
, notre modèle d'héritage remplit donc la définition d'une catégorie. Dans un langage plus traditionnel, nous avons une catégorieClass
avec des objets toutes classes et morphismes les relations d'héritage.C'est un peu de configuration, mais avec cela, jetons un coup d'œil à notre Diamond of Doom:
C'est un diagramme d'apparence douteuse, mais ça fera l'affaire. Ainsi ,
D
hérite de tousA
,B
etC
. En outre, et se rapprocher de la question de OP,D
hérite également de toute superclasse deA
. On peut dessiner un diagrammeMaintenant, les problèmes associés au diamant de la mort ici sont quand
C
etB
partager certains noms de propriété / méthode et les choses deviennent ambiguës; cependant, si nous déplaçons un comportement partagé,A
l'ambiguïté disparaît.En termes catégoriques, nous voulons
A
,B
etC
être tels que siB
etC
hériter deQ
alorsA
peut être réécrit comme une sous-classe deQ
. Cela faitA
quelque chose appelé un pushout .Il existe également une construction symétrique
D
appelée pullback . Il s'agit essentiellement de la classe utile la plus générale que vous puissiez construire et qui hérite à la fois deB
etC
. Autrement dit, si vous avez une autre classe quiR
hérite deB
etC
, alorsD
est une classe oùR
peut être réécrite en tant que sous-classe deD
.S'assurer que vos pointes du diamant sont des pullbacks et des pushouts nous donne un bon moyen de gérer de manière générique les problèmes de conflit de noms ou de maintenance qui pourraient survenir autrement.
Remarque La réponse de Paercebal a inspiré ceci car ses avertissements sont impliqués par le modèle ci-dessus étant donné que nous travaillons dans la catégorie complète Classe de toutes les classes possibles.
Je voulais généraliser son argument à quelque chose qui montre à quel point les relations d'héritage multiples complexes peuvent être à la fois puissantes et non problématiques.
TL; DR Pensez aux relations d'héritage dans votre programme comme formant une catégorie. Ensuite, vous pouvez éviter les problèmes de Diamond of Doom en créant des pushouts de classes à héritage multiple et symétriquement, en créant une classe parente commune qui est un pullback.
la source
Nous utilisons Eiffel. Nous avons un excellent MI. Pas de soucis. Pas d'issues. Facilement géré. Il y a des moments pour NE PAS utiliser MI. Cependant, il est plus utile que les gens ne le pensent parce qu'ils sont: A) dans un langage dangereux qui ne le gère pas bien -OU- B) satisfaits de la façon dont ils ont travaillé autour de l'IM pendant des années et des années -OU- C) d'autres raisons ( trop nombreux pour les énumérer, j'en suis sûr - voir les réponses ci-dessus)
Pour nous, en utilisant Eiffel, MI est aussi naturel que toute autre chose et un autre bel outil dans la boîte à outils. Franchement, nous ne sommes pas du tout préoccupés par le fait que personne d'autre n'utilise Eiffel. Pas de soucis. Nous sommes satisfaits de ce que nous avons et vous invitons à y jeter un œil.
Pendant que vous regardez: prenez note de la sécurité du vide et de l'éradication du déréférencement du pointeur nul. Pendant que nous dansons tous autour de MI, vos pointeurs se perdent! :-)
la source
Chaque langage de programmation a un traitement légèrement différent de la programmation orientée objet avec des avantages et des inconvénients. La version de C ++ met carrément l'accent sur les performances et présente l'inconvénient qu'il est extrêmement facile d'écrire du code invalide - et cela est vrai de l'héritage multiple. En conséquence, il y a une tendance à éloigner les programmeurs de cette fonctionnalité.
D'autres personnes ont abordé la question de savoir à quoi l'héritage multiple n'est pas bon. Mais nous avons vu pas mal de commentaires qui impliquent plus ou moins que la raison de l'éviter est parce que ce n'est pas sûr. Eh bien, oui et non.
Comme c'est souvent le cas en C ++, si vous suivez une directive de base, vous pouvez l'utiliser en toute sécurité sans avoir à «regarder par-dessus votre épaule» constamment. L'idée clé est que vous distinguez un type spécial de définition de classe appelé "mix-in"; class est un mix-in si toutes ses fonctions membres sont virtuelles (ou purement virtuelles). Ensuite, vous êtes autorisé à hériter d'une seule classe principale et d'autant de "mix-ins" que vous le souhaitez - mais vous devez hériter des mixins avec le mot-clé "virtual". par exemple
Ma suggestion est que si vous avez l'intention d'utiliser une classe en tant que classe mixte, vous adoptez également une convention de dénomination pour permettre à quiconque lisant le code de voir ce qui se passe et de vérifier que vous jouez selon les règles de la ligne directrice de base . Et vous constaterez que cela fonctionne beaucoup mieux si vos mix-ins ont également des constructeurs par défaut, simplement à cause du fonctionnement des classes de base virtuelles. Et n'oubliez pas de rendre virtuels tous les destructeurs.
Notez que mon utilisation du mot "mix-in" ici n'est pas la même que la classe de modèle paramétrée (voir ce lien pour une bonne explication) mais je pense que c'est une utilisation juste de la terminologie.
Maintenant, je ne veux pas donner l'impression que c'est la seule façon d'utiliser l'héritage multiple en toute sécurité. C'est juste un moyen assez facile à vérifier.
la source
Vous devez l'utiliser avec précaution, dans certains cas, comme le problème du diamant , les choses peuvent se compliquer.
(source: learncpp.com )
la source
Printer
ne devrait même pas être unPoweredDevice
. APrinter
est pour l'impression, pas pour la gestion de l'alimentation. L'implémentation d'une imprimante particulière peut nécessiter une gestion de l'alimentation, mais ces commandes de gestion de l'alimentation ne doivent pas être exposées directement aux utilisateurs de l'imprimante. Je ne peux imaginer aucune utilisation réelle de cette hiérarchie.Usages et abus de l'héritage.
L'article fait un excellent travail pour expliquer l'héritage et ses dangers.
la source
Au-delà du motif en losange, l'héritage multiple a tendance à rendre le modèle objet plus difficile à comprendre, ce qui à son tour augmente les coûts de maintenance.
La composition est intrinsèquement facile à comprendre, à comprendre et à expliquer. L'écriture de code peut devenir fastidieuse, mais un bon IDE (cela fait quelques années que je n'ai pas travaillé avec Visual Studio, mais les IDE Java ont tous d'excellents outils d'automatisation de raccourcis de composition) devrait vous aider à surmonter cet obstacle.
De plus, en termes de maintenance, le «problème du diamant» se pose également dans les instances d'héritage non littéral. Par exemple, si vous avez A et B et que votre classe C les étend tous les deux, et A a une méthode 'makeJuice' qui fait du jus d'orange et que vous étendez cela pour faire du jus d'orange avec une touche de citron vert: que se passe-t-il lorsque le concepteur pour ' B 'ajoute une méthode' makeJuice 'qui génère un courant électrique? «A» et «B» peuvent être des «parents» compatibles en ce moment , mais cela ne veut pas dire qu'ils le seront toujours!
Dans l'ensemble, la maxime de tendance à éviter l'héritage, et en particulier l'héritage multiple, est bonne. Comme toutes les maximes, il y a des exceptions, mais vous devez vous assurer qu'il y a un néon vert clignotant pointant sur toutes les exceptions que vous codez (et entraînez votre cerveau pour que chaque fois que vous voyez de tels arbres d'héritage, vous dessinez votre propre néon vert clignotant. sign), et que vous vérifiez pour vous assurer que tout a du sens de temps en temps.
la source
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
Euhhh, vous obtenez une erreur de compilation bien sûr (si l'utilisation est ambiguë).Le problème clé avec MI des objets concrets est que rarement vous avez un objet qui devrait légitimement "être un A ET être un B", donc c'est rarement la bonne solution pour des raisons logiques. Beaucoup plus souvent, vous avez un objet C qui obéit à "C peut agir comme un A ou un B", ce que vous pouvez réaliser via l'héritage et la composition d'interface. Mais ne vous y trompez pas, l'héritage de plusieurs interfaces est toujours MI, juste un sous-ensemble de celui-ci.
Pour C ++ en particulier, la principale faiblesse de la fonctionnalité n'est pas la réelle EXISTENCE de l'héritage multiple, mais certaines constructions qu'elle autorise sont presque toujours mal formées. Par exemple, hériter de plusieurs copies du même objet comme:
est mal formé PAR DEFINITION. Traduit en anglais, c'est "B est un A et un A". Donc, même dans le langage humain, il y a une grave ambiguïté. Vouliez-vous dire "B a 2 As" ou simplement "B est un A" ?. Autoriser un tel code pathologique et, pire encore, en faire un exemple d'utilisation, n'a pas aidé C ++ lorsqu'il s'agissait de justifier le maintien de la fonctionnalité dans les langages successeurs.
la source
Vous pouvez utiliser la composition de préférence à l'héritage.
Le sentiment général est que la composition est meilleure, et c'est très bien discuté.
la source
The general feeling is that composition is better, and it's very well discussed.
Cela ne veut pas dire que la composition est meilleure.cela prend 4/8 octets par classe impliquée. (Un ce pointeur par classe).
Cela pourrait ne jamais être un problème, mais si un jour vous avez une structure de micro-données instanciée des milliards de fois, ce sera le cas.
la source