L'héritage multiple est une caractéristique de certains langages de programmation orientés objet dans lesquels un objet ou une classe peut hériter de caractéristiques de plusieurs objets ou classes parents . Il se distingue de l'héritage simple, où un objet ou une classe ne peut hériter que d'un seul objet ou d'une seule classe en particulier.
L'héritage multiple est un problème controversé depuis de nombreuses années ses opposants soulignant sa complexité et son ambiguïté accrues dans des situations telles que le « problème du diamant », où il peut être ambigu de savoir de quelle classe parent une fonctionnalité particulière est héritée si plusieurs classes parentes implémentent ladite fonctionnalité. Cela peut être résolu de diverses manières, notamment en utilisant l' héritage virtuel . Des méthodes alternatives de composition d'objets non basées sur l'héritage, telles que les mixins et les traits, ont également été proposées pour résoudre cette ambiguïté.
Détails
En programmation orientée objet (OOP), l'héritage décrit une relation entre deux classes dans laquelle une classe (la classe enfant ) sous-classe la classe parent . L'enfant hérite des méthodes et des attributs du parent, ce qui permet de partager les fonctionnalités. Par exemple, on peut créer une classe variable Mammal avec des fonctionnalités telles que manger, se reproduire, etc., puis définir une classe enfant Cat qui hérite de ces fonctionnalités sans avoir à les programmer explicitement, tout en ajoutant de nouvelles fonctionnalités comme la chasse aux souris .
L'héritage multiple permet aux programmeurs d'utiliser simultanément plusieurs hiérarchies totalement orthogonales, par exemple en permettant à Cat d'hériter du personnage de dessin animé , de l'animal de compagnie et du mammifère et d'accéder aux fonctionnalités de toutes ces classes.
Implémentations
Les langages qui prennent en charge l'héritage multiple incluent : C++ , Common Lisp (via Common Lisp Object System (CLOS)), EuLisp (via The EuLisp Object System TELOS), Curl , Dylan , Eiffel , Logtalk , Object REXX , Scala (via l'utilisation de classes mixin ), OCaml , Perl , POP-11 , Python , R , Raku et Tcl (intégré à partir de 8.6 ou via Incremental Tcl ( Incr Tcl ) dans les versions antérieures ).
L'environnement d'exécution IBM System Object Model (SOM) prend en charge l'héritage multiple et tout langage de programmation ciblant SOM peut implémenter de nouvelles classes SOM héritées de plusieurs bases.
Certains langages orientés objet, tels que Swift , Java , Fortran depuis sa révision de 2003 , C# et Ruby implémentent l'héritage unique , bien que les protocoles , ou interfaces, fournissent certaines des fonctionnalités du véritable héritage multiple.
PHP utilise des classes de traits pour hériter d'implémentations de méthodes spécifiques. Ruby utilise des modules pour hériter de plusieurs méthodes.
Le problème du diamant

Le « problème du diamant » (parfois appelé le « diamant mortel de la mort » ) est une ambiguïté qui survient lorsque deux classes B et C héritent de A, et que la classe D hérite à la fois de B et de C. S'il existe une méthode dans A que B et C ont remplacée , et que D ne la remplace pas, alors de quelle version de la méthode D hérite-t-il : celle de B, ou celle de C ?
Par exemple, dans le contexte du développement d'un logiciel d'interface utilisateur graphique , une classe peut hériter à la fois des classes (pour l'apparence) et (pour la fonctionnalité/la gestion des entrées), et les classes et les deux héritent de la classe. Maintenant, si la méthode est appelée pour un objet et qu'il n'y a pas de méthode de ce type dans la classe mais qu'il y a une méthode surchargée dans ou (ou les deux), quelle méthode doit être finalement appelée ? ButtonRectangleClickableRectangleClickableObjectequalsButtonButtonequalsRectangleClickable
On l'appelle le « problème du diamant » en raison de la forme du diagramme d'héritage de classe dans cette situation. Dans ce cas, la classe A est en haut, B et C séparément en dessous, et D relie les deux en bas pour former une forme de diamant.
Atténuation
Les langues ont différentes manières de traiter ces problèmes d’héritage répété.
- C# (depuis C# 8.0) autorise l'implémentation de méthodes d'interface par défaut, ce qui fait qu'une classe
A, implémentant des interfacesIaetIbavec des méthodes similaires ayant des implémentations par défaut, a deux méthodes « héritées » avec la même signature, ce qui provoque le problème du diamant. On l'atténue soit en exigeantAl'implémentation de la méthode elle-même, supprimant ainsi l'ambiguïté, soit en forçant l'appelant à d'abord convertir l'Aobjet en l'interface appropriée pour utiliser son implémentation par défaut de cette méthode (par exemple((Ia) aInstance).Method();). - Par défaut, C++ suit chaque chemin d'héritage séparément, donc un
Dobjet contiendrait en fait deuxAobjets distincts, et les utilisations desAmembres de doivent être correctement qualifiées. Si l'héritage deAtoBet l'héritage deAtoCsont tous deux marqués "virtual" (par exemple, "class B : virtual public A"), C++ prend particulièrement soin de ne créer qu'un seulAobjet, et les utilisations desAmembres de fonctionnent correctement. Si l'héritage virtuel et l'héritage non virtuel sont mélangés, il existe un seul virtualAet un non virtualApour chaque chemin d'héritage non virtuel toA. C++ nécessite d'indiquer explicitement à partir de quelle classe parent la fonctionnalité à utiliser est invoquée, c'est-à-direWorker::Human.Age. C++ ne prend pas en charge l'héritage répété explicite car il n'y aurait aucun moyen de qualifier la superclasse à utiliser (c'est-à-dire qu'une classe apparaisse plus d'une fois dans une seule liste de dérivation [class Dog : public Animal, Animal]). C++ permet également de créer une seule instance de la classe multiple via le mécanisme d'héritage virtuel (c'est-à-dire queWorker::HumanetMusician::Humanréférenceront le même objet). - Common Lisp
CLOS tente de fournir à la fois un comportement par défaut raisonnable et la possibilité de le remplacer. Par défaut, pour faire simple, les méthodes sont triées dans
D,B,C,A, lorsque B est écrit avant C dans la définition de classe. La méthode avec les classes d'arguments les plus spécifiques est choisie (D>(B,C)>A) ; puis dans l'ordre dans lequel les classes parentes sont nommées dans la définition de sous-classe (B>C). Cependant, le programmeur peut outrepasser cela, en donnant un ordre de résolution de méthode spécifique ou en énonçant une règle pour combiner les méthodes. C'est ce qu'on appelle la combinaison de méthodes, qui peut être entièrement contrôlée. Le protocole MOP ( metaobject protocol) fournit également des moyens de modifier l'héritage, la répartition dynamique , l'instanciation de classe et d'autres mécanismes internes sans affecter la stabilité du système. - Curl autorise uniquement les classes explicitement marquées comme partagées à être héritées de manière répétée. Les classes partagées doivent définir un constructeur secondaire pour chaque constructeur régulier de la classe. Le constructeur régulier est appelé la première fois que l'état de la classe partagée est initialisé via un constructeur de sous-classe, et le constructeur secondaire sera invoqué pour toutes les autres sous-classes.
- Dans Eiffel , les caractéristiques des ancêtres sont choisies explicitement avec les directives select et rename. Cela permet de partager les caractéristiques de la classe de base entre ses descendants ou de donner à chacun d'eux une copie distincte de la classe de base. Eiffel permet de joindre ou de séparer explicitement les caractéristiques héritées des classes ancêtres. Eiffel joindra automatiquement les caractéristiques ensemble, si elles ont le même nom et la même implémentation. Le rédacteur de la classe a la possibilité de renommer les caractéristiques héritées pour les séparer. L'héritage multiple est un phénomène fréquent dans le développement Eiffel ; la plupart des classes efficaces de la bibliothèque EiffelBase de structures de données et d'algorithmes largement utilisée, par exemple, ont deux parents ou plus.
- Go évite le problème du diamant au moment de la compilation. Si une structure
Dintègre deux structuresBetCqui ont toutes deux une méthodeF(), satisfaisant ainsi une interfaceA, le compilateur se plaindra d'un « sélecteur ambigu » siD.F()est appelé, ou si une instance deDest assignée à une variable de typeA. Les méthodes deBetCpeuvent être appelées explicitement avecD.B.F()ouD.C.F(). - Java 8 introduit des méthodes par défaut sur les interfaces. Si
A,B,Cce sont des interfaces,B,Cchacune peut fournir une implémentation différente à une méthode abstraite deA, ce qui provoque le problème du diamant. L'une ou l'autre classeDdoit réimplémenter la méthode (dont le corps peut simplement transférer l'appel à l'une des super-implémentations), sinon l'ambiguïté sera rejetée comme une erreur de compilation. Avant Java 8, Java n'était pas soumis au risque du problème du diamant, car il ne prenait pas en charge l'héritage multiple et les méthodes par défaut de l'interface n'étaient pas disponibles. - JavaFX Script dans sa version 1.2 permet l'héritage multiple via l'utilisation de mixins . En cas de conflit, le compilateur interdit l'utilisation directe de la variable ou de la fonction ambiguë. Chaque membre hérité reste accessible en convertissant l'objet en mixin d'intérêt, par exemple
(individual as Person).printInfo();. - Kotlin autorise l'héritage multiple d'interfaces. Cependant, dans un scénario de problème Diamond, la classe enfant doit remplacer la méthode qui provoque le conflit d'héritage et spécifier quelle implémentation de classe parent doit être utilisée.
super<ChosenParentInterface>.someMethod() - Logtalk prend en charge l'héritage multiple d'interface et d'implémentation, permettant la déclaration d' alias de méthode qui fournissent à la fois le renommage et l'accès aux méthodes qui seraient masquées par le mécanisme de résolution de conflit par défaut.
- En OCaml , les classes parentes sont spécifiées individuellement dans le corps de la définition de classe. Les méthodes (et les attributs) sont héritées dans le même ordre, chaque méthode nouvellement héritée remplaçant toutes les méthodes existantes. OCaml choisit la dernière définition correspondante d'une liste d'héritage de classe pour déterminer quelle implémentation de méthode utiliser en cas d'ambiguïté. Pour remplacer le comportement par défaut, il suffit de qualifier un appel de méthode avec la définition de classe souhaitée.
- Perl utilise la liste des classes à hériter comme une liste ordonnée. Le compilateur utilise la première méthode qu'il trouve en recherchant en profondeur la liste des superclasses ou en utilisant la linéarisation C3 de la hiérarchie des classes. Diverses extensions fournissent des schémas de composition de classe alternatifs. L'ordre d'héritage affecte la sémantique de la classe. Dans l'ambiguïté ci-dessus, class
Bet ses ancêtres seraient vérifiés avant classCet ses ancêtres, donc la méthode inAserait héritée viaB. Ceci est partagé avec Io et Picolisp . Dans Perl, ce comportement peut être remplacé en utilisant lemroou d'autres modules pour utiliser la linéarisation C3 ou d'autres algorithmes. - Python a la même structure que Perl, mais, contrairement à Perl, l'inclut dans la syntaxe du langage. L'ordre d'héritage affecte la sémantique de la classe. Python a dû gérer cela lors de l'introduction de classes de nouveau style, qui ont toutes un ancêtre commun,
object. Python crée une liste de classes en utilisant l' algorithme de linéarisation C3 (ou Ordre de résolution des méthodes (MRO)). Cet algorithme impose deux contraintes : les enfants précèdent leurs parents et si une classe hérite de plusieurs classes, elles sont conservées dans l'ordre spécifié dans le tuple des classes de base (cependant, dans ce cas, certaines classes situées en haut du graphe d'héritage peuvent précéder les classes situées en bas du graphe ). Ainsi, l'ordre de résolution des méthodes est :D,B,C,A. - Les classes Ruby ont exactement un parent mais peuvent également hériter de plusieurs modules ; les définitions de classe Ruby sont exécutées et la (re)définition d'une méthode masque toute définition préexistante au moment de l'exécution. En l'absence de métaprogrammation d'exécution, cela a à peu près la même sémantique que la résolution en profondeur la plus à droite.
- Scala permet l'instanciation multiple de traits , ce qui permet un héritage multiple en ajoutant une distinction entre la hiérarchie des classes et la hiérarchie des traits. Une classe ne peut hériter que d'une seule classe, mais peut mélanger autant de traits que souhaité. Scala résout les noms de méthode en utilisant une recherche en profondeur de droite en premier des « traits » étendus, avant d'éliminer toutes les occurrences de chaque module dans la liste résultante, sauf la dernière. Ainsi, l'ordre de résolution est : [
D,C,A,B,A], qui se réduit à [D,C,B,A]. - Tcl autorise plusieurs classes parentes ; l'ordre de spécification dans la déclaration de classe affecte la résolution de nom pour les membres utilisant l' algorithme de linéarisation C3 .
Les langages qui n'autorisent qu'un héritage unique , où une classe ne peut dériver que d'une seule classe de base, n'ont pas le problème du diamant. La raison en est que ces langages ont au plus une implémentation de n'importe quelle méthode à n'importe quel niveau de la chaîne d'héritage, indépendamment de la répétition ou du placement des méthodes. Généralement, ces langages permettent aux classes d'implémenter plusieurs protocoles , appelés interfaces en Java. Ces protocoles définissent des méthodes mais ne fournissent pas d'implémentations concrètes. Cette stratégie a été utilisée par ActionScript , C# , D , Java , Nemerle , Object Pascal , Objective-C , Smalltalk , Swift et PHP . Tous ces langages permettent aux classes d'implémenter plusieurs protocoles.
De plus, Ada , C#, Java, Object Pascal, Objective-C, Swift et PHP permettent l'héritage multiple d'interfaces (appelées protocoles en Objective-C et Swift). Les interfaces sont comme des classes de base abstraites qui spécifient des signatures de méthodes sans implémenter de comportement. (Les interfaces « pures » telles que celles de Java jusqu'à la version 7 n'autorisent aucune implémentation ou donnée d'instance dans l'interface.) Néanmoins, même lorsque plusieurs interfaces déclarent la même signature de méthode, dès que cette méthode est implémentée (définie) n'importe où dans la chaîne d'héritage, elle remplace toute implémentation de cette méthode dans la chaîne au-dessus d'elle (dans ses superclasses). Par conséquent, à n'importe quel niveau donné dans la chaîne d'héritage, il ne peut y avoir qu'une seule implémentation de n'importe quelle méthode. Ainsi, l'implémentation d'une méthode à héritage unique ne présente pas le problème du diamant même avec l'héritage multiple d'interfaces. Avec l'introduction de l'implémentation par défaut pour les interfaces dans Java 8 et C# 8, il est toujours possible de générer un problème du diamant, bien que cela n'apparaisse que comme une erreur de compilation.