web-dev-qa-db-fra.com

Y a-t-il une "vraie" raison pour laquelle l'héritage multiple est détesté?

J'ai toujours aimé l'idée d'avoir l'héritage multiple pris en charge dans une langue. Le plus souvent, il est intentionnellement abandonné et le "remplacement" supposé est les interfaces. Les interfaces ne couvrent tout simplement pas le même héritage multiple au sol, et cette restriction peut occasionnellement conduire à plus de code passe-partout.

La seule raison fondamentale que j'ai jamais entendue pour cela est le problème du diamant avec les classes de base. Je ne peux tout simplement pas accepter cela. Pour moi, cela ressemble énormément à: "Eh bien, il est possible de le gâcher, c'est donc automatiquement une mauvaise idée." Vous pouvez tout foutre dans un langage de programmation, et je veux dire n'importe quoi. Je ne peux tout simplement pas prendre cela au sérieux, du moins pas sans une explication plus approfondie.

Être conscient de ce problème représente 90% de la bataille. De plus, je pense avoir entendu il y a quelques années une solution de contournement à usage général impliquant un algorithme "d'enveloppe" ou quelque chose comme ça (cela vous rappelle-t-il quelque chose?).

En ce qui concerne le problème du diamant, le seul problème potentiellement authentique auquel je peux penser est si vous essayez d'utiliser une bibliothèque tierce et ne pouvez pas voir que deux classes apparemment sans rapport dans cette bibliothèque ont une classe de base commune, mais en plus de la documentation, une fonctionnalité de langage simple pourrait, disons, exiger que vous déclariez spécifiquement votre intention de créer un diamant avant de pouvoir en compiler un pour vous. Avec une telle fonctionnalité, toute création d'un diamant est soit intentionnelle, imprudente, soit parce que l'on ignore cet écueil.

Pour que tout soit dit ... Y a-t-il une vraie raison pour laquelle la plupart des gens détestent l'héritage multiple, ou est-ce juste un tas d'hystérie qui cause plus de mal que bon? Y a-t-il quelque chose que je ne vois pas ici? Je vous remercie.

Exemple

La voiture étend WheeledVehicle, KIASpectra étend la voiture et l'électronique, KIASpectra contient la radio. Pourquoi KIASpectra ne contient-il pas d'électronique?

  1. Parce que est un électronique. L'héritage par rapport à la composition doit toujours être une relation est-une vs une relation a-une.

  2. Parce que est un électronique. Il y a des fils, des cartes de circuits imprimés, des commutateurs, etc. tout le long de cette chose.

  3. Parce que est un électronique. Si votre batterie se décharge en hiver, vous avez autant de problèmes que si toutes vos roues manquaient soudainement.

Pourquoi ne pas utiliser des interfaces? Prenez # 3, par exemple. Je ne veux pas écrire cela encore et encore, et je vraiment ne veux pas créer une classe d'assistance proxy bizarre pour le faire non plus:

private void runOrDont()
{
    if (this.battery)
    {
        if (this.battery.working && this.switchedOn)
        {
            this.run();
            return;
        }
    }
    this.dontRun();
}

(Nous ne savons pas si cette implémentation est bonne ou mauvaise.) Vous pouvez imaginer comment il peut y avoir plusieurs de ces fonctions associées à Electronic qui ne sont liées à rien dans WheeledVehicle , et vice versa.

Je ne savais pas trop si je devais m'installer sur cet exemple, car il y avait place à interprétation. Vous pouvez également penser en termes de véhicule étendu et FlyingObject et d'oiseau en étendant Animal et FlyingObject, ou en termes d'un exemple beaucoup plus pur.

129
Panzercrisis

Dans de nombreux cas, les gens utilisent l'héritage pour fournir un trait à une classe. Par exemple, pensez à un Pegasus. Avec l'héritage multiple, vous pourriez être tenté de dire que le Pegasus étend le cheval et l'oiseau parce que vous avez classé l'oiseau comme un animal avec des ailes.

Cependant, les oiseaux ont d'autres traits que Pegasi n'a pas. Par exemple, les oiseaux pondent des œufs, Pegasi a une naissance vivante. Si l'héritage est votre seul moyen de transmettre les traits de partage, il n'y a aucun moyen d'exclure le trait de ponte du Pégase.

Certaines langues ont choisi de faire des traits une construction explicite dans la langue. D'autres vous guident doucement dans cette direction en supprimant MI de la langue. De toute façon, je ne peux pas penser à un seul cas où je pensais "Homme, j'ai vraiment besoin de MI pour le faire correctement".

Discutons également de ce qu'est vraiment l'héritage. Lorsque vous héritez d'une classe, vous dépendez de cette classe, mais vous devez également prendre en charge les contrats pris en charge par cette classe, à la fois implicites et explicites.

Prenons l'exemple classique d'un carré héritant d'un rectangle. Le rectangle expose une propriété length et width ainsi qu'une méthode getPerimeter et getArea. Le carré remplacerait la longueur et la largeur de sorte que lorsque l'un est défini, l'autre est défini pour correspondre à getPerimeter et getArea fonctionnerait de la même manière (2 * longueur + 2 * largeur pour le périmètre et longueur * largeur pour la zone).

Il existe un seul cas de test qui se casse si vous remplacez cette implémentation d'un carré par un rectangle.

var rectangle = new Square();
rectangle.length= 5;
rectangle.width= 6;
Assert.AreEqual(30, rectangle.GetArea()); 
//Square returns 36 because setting the width clobbers the length

C'est assez difficile pour bien faire les choses avec une seule chaîne d'héritage. Cela devient encore pire lorsque vous en ajoutez un autre au mélange.


Les pièges que j'ai mentionnés avec le Pegasus in MI et les relations Rectangle/Carré sont tous les deux le résultat d'une conception inexpérimentée pour les classes. En gros, éviter l'héritage multiple est un moyen d'aider les développeurs débutants à ne pas se tirer une balle dans le pied. Comme tous les principes de conception, une discipline et une formation basées sur eux vous permettent de découvrir à temps quand vous pouvez vous en détacher. Voir le Dreyfus Model of Skill Acquisition , au niveau Expert, vos connaissances intrinsèques transcendent la dépendance aux maximes/principes. Vous pouvez "sentir" lorsqu'une règle ne s'applique pas.

Et je suis d'accord que j'ai un peu triché avec un exemple "du monde réel" de la raison pour laquelle MI est mal vu.

Regardons un cadre d'interface utilisateur. Examinons plus particulièrement quelques widgets qui pourraient à première vue ressembler à une simple combinaison de deux autres. Comme un ComboBox. Un ComboBox est un TextBox qui a un DropDownList de support. C'est à dire. Je peux taper une valeur ou choisir parmi une liste de valeurs préétablie. Une approche naïve serait d'hériter du ComboBox de TextBox et DropDownList.

Mais votre Textbox tire sa valeur de ce que l'utilisateur a tapé. Alors que le DDL tire sa valeur de ce que l'utilisateur sélectionne. Qui a un précédent? La DDL a peut-être été conçue pour vérifier et rejeter toute entrée qui ne figurait pas dans sa liste de valeurs d'origine. Allons-nous outrepasser cette logique? Cela signifie que nous devons exposer la logique interne pour que les héritiers l'ignorent. Ou pire, ajoutez de la logique à la classe de base qui n'est là que pour prendre en charge une sous-classe (violant le Dependency Inversion Principle ).

Éviter l'IM vous aide à éviter complètement cet écueil. Et cela pourrait vous amener à extraire des traits communs et réutilisables de vos widgets d'interface utilisateur afin qu'ils puissent être appliqués au besoin. Un excellent exemple de ceci est la WPF Attached Property qui permet à un élément de framework dans WPF de fournir une propriété qu'un autre élément de framework peut utiliser sans hériter de l'élément de framework parent.

Par exemple, une Grille est un panneau de disposition dans WPF et il a des propriétés attachées Colonne et Ligne qui spécifient où un élément enfant doit être placé dans la disposition de la grille. Sans propriétés attachées, si je veux organiser un bouton dans une grille, le bouton devrait dériver de la grille afin qu'il puisse avoir accès aux propriétés de la colonne et de la ligne.

Les développeurs ont poussé ce concept plus loin et ont utilisé les propriétés attachées comme moyen de composant le comportement (par exemple, voici mon article sur la création d'un GridView triable à l'aide des propriétés attachées écrit avant que WPF n'inclue un DataGrid). L'approche a été reconnue comme un modèle de conception XAML appelé Comportements attachés .

J'espère que cela a permis de mieux comprendre pourquoi l'héritage multiple est généralement mal vu.

70
Michael Brown

Y a-t-il quelque chose que je ne vois pas ici?

Autoriser l'héritage multiple rend les règles concernant les surcharges de fonctions et la répartition virtuelle nettement plus délicates, ainsi que l'implémentation du langage autour des dispositions d'objets. Ils ont un impact considérable sur les concepteurs/implémenteurs de langage et élèvent la barre déjà élevée pour obtenir un langage fait, stable et adopté.

Un autre argument commun que j'ai vu (et avancé parfois) est qu'en ayant deux classes de base +, votre objet viole presque toujours le principe de responsabilité unique. Soit les deux classes de base + sont des classes autonomes Nice avec leur propre responsabilité (provoquant la violation), soit ce sont des types partiels/abstraits qui travaillent ensemble pour créer une seule responsabilité cohérente.

Dans cet autre cas, vous avez 3 scénarios:

  1. A ne sait rien de B - Super, vous pouvez combiner les cours parce que vous avez eu de la chance.
  2. A connaît B - Pourquoi A n'a-t-il pas simplement hérité de B?
  3. A et B se connaissent - Pourquoi n'avez-vous pas fait un seul cours? Quel est l'avantage de rendre ces choses si couplées mais partiellement remplaçables?

Personnellement, je pense que l'héritage multiple a un mauvais rap, et qu'un système bien fait de composition de style de trait serait vraiment puissant/utile ... mais il y a beaucoup de façons dont il peut être mal mis en œuvre, et beaucoup de raisons pour lesquelles il est pas une bonne idée dans un langage comme C++.

[modifier] concernant votre exemple, c'est absurde. Un Kia a électronique. Il a un moteur. De même, c'est l'électronique avoir une source d'alimentation, qui se trouve être une batterie de voiture. L'héritage, sans parler de l'héritage multiple, n'a pas sa place.

60
Telastyn

La seule raison pour laquelle il est interdit est qu'il permet aux gens de se tirer une balle dans le pied.

Ce qui suit habituellement dans ce genre de discussion, ce sont des arguments pour savoir si la flexibilité d'avoir les outils est plus importante que la sécurité de ne pas tirer du pied. Il n'y a pas de réponse résolument correcte à cet argument, car comme la plupart des autres choses en programmation, la réponse dépend du contexte.

Si vos développeurs sont à l'aise avec MI, et que MI a du sens dans le contexte de ce que vous faites, alors vous le regretterez cruellement dans un langage qui ne le prend pas en charge. En même temps, si l'équipe n'est pas à l'aise avec cela, ou si elle n'en a pas vraiment besoin et que les gens l'utilisent "simplement parce qu'ils le peuvent", cela est contre-productif.

Mais non, il n’existe pas d’argument absolument convaincant qui prouve que l’héritage multiple est une mauvaise idée.

MODIFIER

Les réponses à cette question semblent être unanimes. Pour être l'avocat du diable, je fournirai un bon exemple d'héritage multiple, où ne pas le faire mène à des hacks.

Supposons que vous conceviez une application pour les marchés de capitaux. Vous avez besoin d'un modèle de données pour les titres. Certains titres sont des produits d'actions (actions, fiducies de placement immobilier, etc.), d'autres sont des dettes (obligations, obligations de sociétés), d'autres sont des produits dérivés (options, futures). Donc, si vous évitez l'IM, vous allez créer un arbre d'héritage très clair et simple. Une action héritera des capitaux propres, une obligation héritera de la dette. Très bien jusqu'à présent, mais qu'en est-il des dérivés? Ils peuvent être basés sur des produits de type actions ou des produits de type débit? Ok, je suppose que nous allons élargir notre branche d'arbre d'héritage. Gardez à l'esprit que certains dérivés sont basés sur des produits d'actions, des produits de dette ou aucun des deux. Notre arbre d'héritage se complique donc. Vient ensuite l'analyste d'affaires et vous dit que nous prenons désormais en charge les titres indexés (options d'index, options d'index futures). Et ces choses peuvent être basées sur les capitaux propres, la dette ou les dérivés. Cela devient désordonné! Est-ce que mon option future sur indice dérive Equity-> Stock-> Option-> Index? Pourquoi pas Actions-> Actions-> Index-> ​​Option? Et si un jour je trouve les deux dans mon code (c'est arrivé; histoire vraie)?

Le problème ici est que ces types fondamentaux peuvent être mélangés dans n'importe quelle permutation qui ne dérive pas naturellement l'un de l'autre. Les objets sont définis par une relation est une relation , donc la composition n'a aucun sens. L'héritage multiple (ou le concept similaire de mixins) est la seule représentation logique ici.

La vraie solution à ce problème consiste à définir et à mélanger les types Equity, Debt, Derivative, Index en utilisant l'héritage multiple pour créer votre modèle de données. Cela créera des objets qui auront du sens et se prêteront facilement à la réutilisation du code.

29
MrFox

Les autres réponses semblent ici entrer principalement dans la théorie. Voici donc un exemple concret Python simplifié vers le bas) dans lequel je me suis écrasé tête baissée, ce qui a nécessité une bonne quantité de refactoring:

class Foo(object):
   def zeta(self):
      print "foozeta"

class Bar(object):
   def zeta(self):
      print "barzeta"

   def barstuff(self):
      print "barstuff"
      self.zeta()

class Bang(Foo, Bar):
   def stuff(self):
      self.zeta()
      print "---"
      self.barstuff()

z = Bang()
z.stuff()

Bar a été écrit en supposant qu'il avait sa propre implémentation de zeta(), ce qui est généralement une assez bonne hypothèse. Une sous-classe doit la remplacer de manière appropriée afin de faire la bonne chose. Malheureusement, les noms n'étaient que par coïncidence les mêmes - ils faisaient des choses plutôt différentes, mais Bar appelait maintenant l'implémentation de Foo:

foozeta
---
barstuff
foozeta

C'est assez frustrant quand il n'y a pas d'erreurs levées, l'application commence à agir si légèrement que mal, et le changement de code qui l'a causé (créant Bar.zeta) Ne semble pas être à l'origine du problème.

11
Izkata

Je dirais qu'il n'y a pas de vrai problème avec MI dans la bonne langue . La clé est d'autoriser les structures losanges, mais d'exiger que les sous-types fournissent leur propre remplacement, au lieu que le compilateur ne choisisse l'une des implémentations en fonction d'une règle.

Je le fais en Guava , une langue sur laquelle je travaille. Une caractéristique de Guava est que nous pouvons invoquer l'implémentation d'une méthode par un supertype spécifique. Il est donc facile d'indiquer quelle implémentation de supertype doit être "héritée", sans syntaxe particulière:

type Sequence[+A] {
  String toString() {
    return "[" + ... + "]";
  }
}

type Set[+A] {
  String toString() {
    return "{" + ... + "}";
  }
}

type OrderedSet[+A] extends Sequence[A], Set[A] {
  String toString() {
    // This is Guava's syntax for statically invoking instance methods
    return Set.toString(this);
  }
}

Si nous ne donnions pas à OrderedSet son propre toString, nous obtiendrions une erreur de compilation. Pas de surprises.

Je trouve que MI est particulièrement utile avec les collections. Par exemple, j'aime utiliser un type RandomlyEnumerableSequence pour éviter de déclarer getEnumerator pour les tableaux, les deques, etc.:

type Enumerable[+A] {
  Source[A] getEnumerator();
}

type Sequence[+A] extends Enumerable[A] {
  A get(Int index);
}

type RandomlyEnumerableSequence[+A] extends Sequence[A] {
  Source[A] getEnumerator() {
    ...
  }
}

type DynamicArray[A] extends MutableStack[A],
                             RandomlyEnumerableSequence[A] {
  // No need to define getEnumerator.
}

Si nous n'avions pas MI, nous pourrions écrire un RandomAccessEnumerator pour plusieurs collections à utiliser, mais le fait d'avoir à écrire une brève méthode getEnumerator ajoute toujours un passe-partout.

De même, MI est utile pour hériter des implémentations standard de equals, hashCode et toString pour les collections.

9
Daniel Lubarov

L'héritage, multiple ou non, n'est pas si important. Si deux objets de type différent sont substituables, c'est ce qui compte, même s'ils ne sont pas liés par héritage.

Une liste liée et une chaîne de caractères ont peu en commun et n'ont pas besoin d'être liées par héritage, mais c'est utile si je peux utiliser une fonction length pour obtenir le nombre d'éléments dans l'un ou l'autre.

L'héritage est une astuce pour éviter l'implémentation répétée du code. Si l'héritage vous permet d'économiser du travail et que l'héritage multiple vous fait gagner encore plus de travail par rapport à l'héritage unique, alors c'est toute la justification qui est nécessaire.

Je soupçonne que certaines langues n'implémentent pas très bien l'héritage multiple, et pour les praticiens de ces langues, c'est ce que signifie l'héritage multiple. Mentionnez l'héritage multiple à un programmeur C++, et ce qui vient à l'esprit est quelque chose à propos des problèmes lorsqu'une classe se retrouve avec deux copies d'une base via deux chemins d'héritage différents, et s'il faut utiliser virtual sur une classe de base, et confusion sur la façon dont les destructeurs sont appelés, etc.

Dans de nombreuses langues, l'héritage de classe est confondu avec l'héritage de symboles. Lorsque vous dérivez une classe D d'une classe B, non seulement vous créez une relation de type, mais parce que ces classes servent également d'espaces de noms lexicaux, vous devez gérer l'importation de symboles de l'espace de noms B vers l'espace de noms D, en plus de la sémantique de ce qui se passe avec les types B et D eux-mêmes. L'héritage multiple pose donc des problèmes de conflit de symboles. Si nous héritons de card_deck et graphic, tous deux "ayant" une méthode draw, qu'est-ce que cela signifie pour draw l'objet résultant? Un système d'objets qui n'a pas ce problème est celui du LISP commun. Peut-être pas par hasard, l'héritage multiple est utilisé dans les programmes LISP.

Mal implémenté, tout ce qui dérange (comme l'héritage multiple) devrait être détesté.

7
Kaz

Pour autant que je sache, une partie du problème (en plus de rendre votre conception un peu plus difficile à comprendre (néanmoins plus facile à coder)) est que le compilateur va économiser suffisamment d'espace pour vos données de classe, ce qui permet une énorme quantité de perte de mémoire dans le cas suivant:

(Mon exemple n'est peut-être pas le meilleur, mais essayez d'obtenir le Gist sur plusieurs espaces mémoire dans le même but, c'est la première chose qui m'est venue à l'esprit: P)

Concider un DDD où le chien de classe s'étend de canin et d'animal familier, un canin a une variable qui indique la quantité de nourriture qu'il devrait manger (un entier) sous le nom dietKg, mais un animal de compagnie a également une autre variable à cet effet généralement sous le même nom (sauf si vous définissez un autre nom de variable, vous aurez alors à codifier du code supplémentaire, qui était le problème initial que vous vouliez éviter, pour gérer et conserver l'intégrité des variables bouth), alors vous aurez deux espaces mémoire pour l'exact Dans le même but, pour éviter cela, vous devrez modifier votre compilateur pour reconnaître ce nom sous le même espace de noms et simplement affecter un seul espace mémoire à ces données, ce qui est malheureusement impossible à déterminer dans le temps de compilation.

Vous pouvez, bien sûr, concevoir une langue pour spécifier qu'une telle variable peut déjà avoir un espace défini ailleurs, mais à la fin, le programmeur doit spécifier où se trouve cet espace mémoire auquel cette variable fait référence (et encore du code supplémentaire).

Croyez-moi, les personnes qui mettent en œuvre cette pensée ont vraiment réfléchi à tout cela, mais je suis heureux que vous ayez demandé, votre aimable perspective est celle qui change les paradigmes;), et je ne dis pas que c'est impossible (mais de nombreuses hypothèses et un compilateur multiphasé doit être implémenté, et un très complexe), je dis juste qu'il n'existe pas encore, si vous démarrez un projet pour votre propre compilateur capable de faire "ceci" (héritage multiple), faites-le moi savoir, je Je serai ravi de rejoindre votre équipe.

1
Ordiel