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


123

J'ai toujours aimé l'idée de prendre en charge l'héritage multiple dans une langue. Le plus souvent, c'est intentionnellement abandonné et le supposé "remplaçant" est une interface. Les interfaces ne couvrent tout simplement pas la même terre sur plusieurs héritages multiples, et cette restriction peut parfois conduire à plus de code passe-partout.

La seule raison fondamentale que j'ai jamais entendue pour cela est le problème des diamants avec les classes de base. Je ne peux tout simplement pas accepter ça. Pour moi, ça vient énormément du genre: "Eh bien, il est possible de tout foirer, alors c'est automatiquement une mauvaise idée." Vous pouvez tout gâcher 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 sans une explication plus approfondie.

Le seul fait d'être conscient de ce problème représente 90% de la bataille. De plus, je pense avoir entendu parler il y a quelques années d'une solution polyvalente impliquant un algorithme "enveloppe" ou quelque chose du genre (est-ce que cela vous dit quelque chose, que ce soit?).

En ce qui concerne le problème des diamants, le seul problème potentiellement sérieux auquel je puisse penser est si vous essayez d'utiliser une bibliothèque tierce et que vous ne pouvez pas voir que deux classes apparemment non liées de cette bibliothèque ont une classe de base commune, mais en plus de La documentation, une fonctionnalité de langage simple pourrait, par exemple, exiger que vous déclariez spécifiquement votre intention de créer un diamant avant qu'il n'en compile réellement un pour vous. Avec une telle caractéristique, toute création d’un diamant est soit intentionnelle, soit imprudente, soit parce que nous ne sommes pas conscients de ce piège.

Pour que tout soit dit… Y a-t-il une raison réelle pour laquelle la plupart des gens détestent l'héritage multiple ou est-ce simplement une hystérie qui fait plus de mal que de bien? Y a-t-il quelque chose que je ne vois pas ici? Je vous remercie.

Exemple

Car étend WheeledVehicle, KIASpectra étend Car et Electronic, KIASpectra contient la radio. Pourquoi KIASpectra ne contient-il pas Electronic?

  1. Parce qu'il est un électronique. L'héritage par rapport à la composition doit toujours être une relation is-a versus une relation has-a.

  2. Parce qu'il est un électronique. Il y a des fils, des cartes de circuits imprimés, des commutateurs, etc.

  3. Parce qu'il est un électronique. Si votre batterie s'épuise en hiver, vous aurez autant de problèmes que si toutes vos roues venaient à disparaître.

Pourquoi ne pas utiliser les interfaces? Prenez le numéro 3, par exemple. Je ne veux pas écrire ceci encore et encore, et je ne veux vraiment 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 n'essayons pas de savoir si cette implémentation est bonne ou mauvaise.) Vous pouvez imaginer comment plusieurs de ces fonctions associées à Electronic peuvent ne pas être liées à WheeledVehicle, et inversement.

Je ne savais pas trop si je devais me calmer sur cet exemple, car il y avait place pour l'interprétation. Vous pouvez également penser en termes d’avion et de FlyingObject et d’Animal et de FlyingObject, ou en tant qu’exemple beaucoup plus pur.


24
il encourage également l' héritage sur la composition ... (quand il devrait être l'inverse)
freak cliquet

34
"Vous pouvez tout gâcher" est une raison parfaitement valable pour supprimer une fonctionnalité. La conception des langues modernes est quelque peu éclatée sur ce point, mais vous pouvez généralement classer les langues en "Pouvoir de restriction" et "Pouvoir de flexibilité". Je ne pense pas que ni l'un ni l'autre ne soit "correct", ils ont tous deux des arguments forts à faire valoir. MI est une de ces choses qui est utilisée pour le mal le plus souvent, et donc les langues restrictives le suppriment. Ceux qui sont flexibles ne le font pas, car "le plus souvent" n'est pas "littéralement toujours". Cela dit, les mixins / traits sont une meilleure solution pour le cas général, je pense.
Phoshi

8
Il existe des alternatives plus sûres à l'héritage multiple dans plusieurs langues, qui vont au-delà des interfaces. Découvrez Scala Traits- elles agissent comme des interfaces avec une implémentation optionnelle, mais ont certaines restrictions qui aident à prévenir des problèmes tels que le problème des diamants.
KChaloux

5
Voir aussi cette question précédemment fermée: programmers.stackexchange.com/questions/122480/…
Doc Brown

19
KiaSpectran'est pas un Electronic ; il a l' électronique, et peut être un ElectronicCar(qui s'étendrait Car...)
Brian S

Réponses:


68

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

Cependant, les oiseaux ont d'autres traits que le Pegasi. Par exemple, les oiseaux pondent, 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 Pegasus.

Certaines langues ont choisi de faire des traits une construction explicite au sein de la langue. Other's vous guide doucement dans cette direction en supprimant MI de la langue. De toute façon, je ne peux penser à un seul cas où je me suis dit: "J'ai vraiment besoin de MI pour le faire correctement".

Aussi, discutons de ce qu'est VRAIMENT l'héritage. Lorsque vous héritez d'une classe, vous prenez une dépendance sur cette classe, mais vous devez également prendre en charge les contrats pris en charge par la 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'une est définie, l'autre est définie de manière à correspondre à getPerimeter et que getArea fonctionne de la même manière (2 * longueur + 2 * largeur pour le périmètre et longueur * largeur pour la surface).

Un cas de test unique se brise si vous substituez cette implémentation d'un carré à 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

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


Les pièges que j'ai mentionnés avec le Pegasus in MI et les relations Rectangle / Carré résultent tous deux d'une conception inexpérimentée pour les classes. En gros, éviter les héritages multiples 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, disposer de la discipline et de la formation qui en découlent vous permet de découvrir à temps quand il convient de rompre avec eux. Voir le modèle d'acquisition de compétences Dreyfus . Au niveau expert, vos connaissances intrinsèques transcendent la confiance en des maximes / principes. Vous pouvez "sentir" quand une règle ne s'applique pas.

Et je conviens que j'ai quelque peu triché avec un exemple "réel" de la raison pour laquelle MI est mal vu.

Regardons un cadre d'interface utilisateur. Plus précisément, examinons quelques widgets qui pourraient au premier abord ressembler à une combinaison de deux autres. Comme une ComboBox. Une ComboBox est une TextBox qui a une DropDownList supportée. C'est-à-dire que je peux saisir une valeur ou sélectionner une liste de valeurs pré-ordonnée. Une approche naïve consisterait à hériter de la ComboBox de TextBox et de 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 la priorité? Le DDL peut avoir été conçu pour vérifier et rejeter toute entrée qui ne figurait pas dans sa liste de valeurs d'origine. Avons-nous outrepasser cette logique? Cela signifie que nous devons exposer la logique interne pour que les héritiers puissent remplacer. Ou pire, ajoutez à la classe de base une logique qui n’existe que pour prendre en charge une sous-classe (en violation du principe d’inversion de dépendance ).

Eviter MI vous aide à éviter ce piège. 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. La propriété attachée WPF, qui permet à un élément de cadre dans WPF de fournir une propriété qu'un autre élément de cadre peut utiliser sans hériter de l'élément de cadre parent, constitue un excellent exemple .

Par exemple, une grille est un panneau de présentation dans WPF et possède des propriétés attachées Column et Row qui spécifient où un élément enfant doit être placé dans la disposition de la grille. Sans les propriétés attachées, si je veux organiser un bouton dans une grille, le bouton devrait dériver de la grille pour pouvoir accéder aux propriétés Column et Row.

Les développeurs ont poussé ce concept plus loin et ont utilisé les propriétés attachées comme moyen de comportement (par exemple, voici mon article sur la création d'un GridView triable à l'aide de propriétés attachées écrites 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.


14
"Man, j'ai vraiment besoin de MI pour le faire correctement" - voir ma réponse pour un exemple. En un mot, les concepts orthogonaux qui satisfont une relation is-a ont du sens pour MI. Ils sont très rares, mais ils existent.
MrFox

13
Mec, je déteste cet exemple de rectangle carré. Il existe de nombreux exemples plus éclairants qui ne nécessitent pas de mutabilité.
Asmeurer

9
J'aime que la réponse à "donner un exemple réel" était de discuter d'une créature de fiction. Excellente ironie +1
jjathman

3
Vous dites donc que l'héritage multiple est mauvais, parce que l'héritage est mauvais (vos exemples concernent l'héritage à une seule interface). Par conséquent, sur la base de cette seule réponse, un langage doit soit se débarrasser de l'héritage et des interfaces, soit disposer de plusieurs héritages.
ctrl-alt-delor

12
Je ne pense pas que ces exemples fonctionnent vraiment ... personne ne dit qu'un pégase est un oiseau et un cheval ... vous dites que c'est un WingedAnimal et un HoofedAnimal. L'exemple carré et rectangle a un peu plus de sens car vous vous trouvez avec un objet qui se comporte différemment que vous ne le pensez en fonction de sa définition affirmée. Cependant, je pense que cette situation serait la faute des programmeurs pour ne pas l’avoir attrapée (s’il s’agissait effectivement d’un problème).
richard

60

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

Autoriser l'héritage multiple rend les règles sur les surcharges de fonctions et la répartition virtuelle nettement plus délicates, de même que la mise en œuvre du langage autour de la disposition des objets. Celles-ci ont un impact considérable sur les concepteurs / implémenteurs de langages et soulèvent la barre déjà haute pour obtenir un langage stable et adopté.

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

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 sait à propos de B - Pourquoi A n'a-t-il pas hérité de B?
  3. A et B se connaissent - Pourquoi n'avez-vous pas fait un cours? Quel avantage y a-t-il à rendre ces éléments si couplés mais partiellement remplaçables?

Personnellement, je pense que l’héritage multiple a un mauvais coup, 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 de le mal mettre en œuvre, pas une bonne idée dans un langage comme C ++.

[edit] en ce qui concerne votre exemple, c'est absurde. Une Kia a de l' électronique. Il a un moteur. De même, ses composants électroniques ont une source d'alimentation, qui se trouve être une batterie de voiture. L'héritage, encore moins l'héritage multiple n'y a pas sa place.


8
Une autre question intéressante est de savoir comment les classes de base + leurs classes de base sont initialisées. Encapsulation> Héritage.
Jozefg

"un système bien fait de composition de style de trait serait vraiment puissant / utile ..." - Celles-ci sont appelées mixins , et elles sont puissantes / utiles. Les mixins peuvent être implémentés à l'aide de MI, mais l' héritage multiple n'est pas requis pour les mixins. Certaines langues supportent les mixins de manière inhérente, sans MI.
BlueRaja - Danny Pflughoeft

Pour Java en particulier, nous avons déjà plusieurs interfaces et INVOKEINTERFACE, de sorte que MI n’aurait aucune incidence évidente sur les performances.
Daniel Lubarov

Tetastyn, je conviens que l'exemple était mauvais et n'a pas servi sa question. L'héritage n'est pas pour les adjectifs (électroniques), c'est pour les noms (véhicule).
richard

29

La seule raison pour laquelle il est interdit, c’est qu’il est plus facile pour les gens de se tirer une balle dans le pied.

Ce qui suit généralement dans ce type 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 se décocher. Il n’existe pas de réponse tout à fait correcte à cet argument car, comme la plupart des autres éléments de la 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, vous le manquerez 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'est pas réellement nécessaire et si les gens l'utilisent «simplement parce qu'ils le peuvent», alors c'est contre-productif.

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

MODIFIER

Les réponses à cette question semblent être unanimes. Par souci d’être l’avocat du diable, je donnerai un bon exemple d’héritage multiple, où ne pas le faire conduit à des piratages.

Supposons que vous concevez une application sur les marchés financiers. Vous avez besoin d'un modèle de données pour les titres. Certains titres sont des produits d’actions (actions, sociétés de placement immobilier, etc.), d’autres des emprunts (obligations, obligations de sociétés), d’autres des produits dérivés (options, contrats à terme). Donc, si vous évitez MI, vous ferez un arbre d'héritage très clair et simple. Une action héritera de l'équité, Bond héritera de la dette. Super jusqu'ici, mais qu'en est-il des dérivés? Ils peuvent être basés sur des produits de type équité ou des produits de type débit? Ok, je suppose que nous allons créer plus de branches dans notre héritage. N'oubliez pas que certains dérivés sont basés sur des produits de capitaux propres, des produits de dette ou aucun des deux. Donc, notre arbre d'héritage devient compliqué. Ensuite, l’analyste commercial nous dit que nous prenons désormais en charge les titres indexés (options d’indice, options futures d’index). Et ces éléments peuvent être basés sur les capitaux propres, la dette ou les dérivés. Cela devient désordonné! Est-ce que l'option future de mon indice dérive Equity-> Stock-> Option-> Index? Pourquoi pas Equity-> Stock-> Index-> ​​Option? Et si un jour je trouve les deux dans mon code (Ceci est arrivé; histoire vraie)?

Le problème ici est que ces types fondamentaux peuvent être mélangés dans toute permutation qui ne dérive pas naturellement l'un de l'autre. Les objets sont définis par un 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 Équité, Dette, Dérivée, Index, en utilisant un héritage multiple pour créer votre modèle de données. Cela créera des objets qui, à la fois, auront du sens et faciliteront la réutilisation du code.


27
J'ai travaillé dans la finance. Il y a une raison pour laquelle la programmation fonctionnelle est préférée à OO dans les logiciels financiers. Et vous l'avez signalé. MI ne résout pas ce problème, il l'exacerbe. Croyez-moi, je ne peux pas vous dire combien de fois j'ai entendu dire: "C'est comme ça ... sauf" quand je traite avec des clients de la finance, sauf que cela devient le fléau de mon existence.
Michael Brown

8
Cela semble très facile à résoudre avec les interfaces pour moi ... en fait, il semble plus adapté aux interfaces qu'autre chose. Equityet les Debtdeux implémentent ISecurity. Derivativea une ISecuritypropriété. Il se peut que ce soit lui-même un ISecuritysi approprié (je ne connais pas les finances). IndexedSecuritiescontient à nouveau une propriété d’interface qui est appliquée aux types sur lesquels elle est autorisée. S'ils sont tous ISecurity, ils ont tous des ISecuritypropriétés et peuvent être arbitrairement imbriquées ...
Bobson

11
@Bobson le problème vient du fait que vous devez ré-implémenter l'interface pour chaque implémenteur, et dans de nombreux cas, c'est la même chose. Vous pouvez utiliser la délégation / composition mais vous perdez alors l'accès aux membres privés. Et comme Java n'a pas de délégués / lambdas ... il n'y a littéralement pas de bon moyen de le faire. Sauf éventuellement avec un outil Aspect comme Aspect4J
Michael Brown

10
@Bobson exactement ce que Mike Brown a dit. Oui, vous pouvez concevoir une solution avec des interfaces, mais ce sera maladroit. Votre intuition d’utiliser des interfaces est très correcte, c’est un désir caché de mixins / multiple multiple :).
MrFox

11
Cela touche une bizarrerie où les programmeurs orientés objet préfèrent penser en termes d'héritage et échouent souvent à accepter la délégation comme une approche OO valide, qui donne généralement une solution plus allégée, plus maintenable et plus extensible. Ayant travaillé dans la finance et les soins de santé, j'ai vu le gâchis ingérable que l’IM peut créer, d’autant plus que les lois fiscales et sanitaires changent d’une année à l’autre et que la définition d’un objet DERNIÈRE année n’est pas valable pour CET année (mais doit néanmoins remplir la fonction de l’une ou l’autre année). et doit être interchangeable). La composition produit un code plus simple, plus facile à tester et moins coûteux.
Shaun Wilson

11

Les autres réponses ici semblent entrer principalement dans la théorie. Voici donc un exemple concret en Python, simplifié, dans lequel je me suis effondré, qui nécessitait pas mal 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()

Bara été écrit en supposant qu’il avait sa propre implémentation zeta(), ce qui est généralement une très bonne hypothèse. Une sous-classe doit la remplacer comme il convient afin de faire la bonne chose. Malheureusement, les noms ne sont que par coïncidence identiques: ils ont fait des choses assez différentes, mais Barappelaient maintenant l' Fooimplémentation:

foozeta
---
barstuff
foozeta

Il est plutôt frustrant de constater qu’aucune erreur n’est générée, que l’application commence à agir de manière légèrement erronée et que le changement de code qui l’a provoquée (création Bar.zeta) ne semble pas être à l’origine du problème.


Comment le problème Diamond est-il évité par les appels super()?
Panzercrisis

@ Panzercrisis Désolé, ce n'est pas grave. Je misremembered quel problème le « problème de diamant » se réfère généralement à - Python avait une question distincte causée par l' héritage en forme de losange qui super()a environ
Izkata

5
Je suis heureux que le MI de C ++ soit bien mieux conçu. Le bug ci-dessus n'est tout simplement pas possible. Vous devez le désambiguïser manuellement avant même que le code ne soit compilé.
Thomas Eding

2
Le fait de modifier l’ordre d’héritage en Bangen Bar, Foorésoudrait également le problème - mais votre argument est bon, +1.
Sean Vieira

1
@ThomasEding Vous pouvez manuellement désambiguïser encore: Bar.zeta(self).
Tyler Crompton

9

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

Je le fais en goyave , une langue sur laquelle je travaille. Une caractéristique de Guava est que nous pouvons invoquer l'implémentation d'une méthode d'un super-type 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 OrderedSetles siennes toString, nous aurions une erreur de compilation. Pas de surprises.

Je trouve que MI est particulièrement utile pour les collections. Par exemple, j'aime bien utiliser un RandomlyEnumerableSequencetype pour éviter de déclarer getEnumeratorpour les tableaux, 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 en écrire un RandomAccessEnumeratorpour plusieurs collections, mais le fait d'écrire une getEnumeratorméthode brève ajoute toujours un passe-partout.

De même, MI est utile pour héritant des implémentations standard de equals, hashCodeet toStringpour les collections.


1
@AndyzSmith, je comprends votre point de vue, mais tous les conflits d'héritage ne sont pas de vrais problèmes sémantiques. Prenons mon exemple: aucun des types ne fait de promesse concernant toStringle comportement de celui-ci. Par conséquent, le fait de passer outre son comportement n'enfreint pas le principe de substitution. Vous obtenez parfois aussi des "conflits" entre des méthodes qui ont le même comportement mais des algorithmes différents, en particulier avec des collections.
Daniel Lubarov

1
@Asmageddon est d'accord, mes exemples impliquent uniquement des fonctionnalités. Selon vous, quels sont les avantages de mixins? Au moins à Scala, les traits ne sont essentiellement que des classes abstraites qui permettent MI - ils peuvent toujours être utilisés pour l'identité tout en apportant le problème des diamants. Existe-t-il d'autres langues où les mixins ont des différences plus intéressantes?
Daniel Lubarov

1
Les classes abstraites sur le plan technique peuvent être utilisées comme interfaces. Pour moi, l'avantage est simplement le fait que la construction elle-même est un "conseil d'utilisation", ce qui signifie que je sais à quoi m'attendre lorsque je vois le code. Cela dit, je code actuellement en Lua, qui n’a pas de modèle POO inhérent - les systèmes de classes existants permettent généralement d’inclure des tables en tant que mixins, et celui que je code utilise des classes en tant que mixins. La seule différence est que vous ne pouvez utiliser que l'héritage (unique) pour l'identité, les mixins pour la fonctionnalité (sans informations d'identité) et les interfaces pour des contrôles ultérieurs. Peut-être que ce n'est pas mieux, mais c'est logique pour moi.
Llamageddon

2
Pourquoi appelez-vous le Ordered extends Set and Sequencea Diamond? C'est juste une jointure. Il manque le hatsommet. Pourquoi appelez-vous cela un diamant? J'ai demandé ici mais cela semble une question tabou. Comment avez-vous su que vous deviez appeler cette structure triangualar un Diamond plutôt que Join?
Val

1
@RecognizeEvilasWaste ce langage a un Toptype implicite qui déclare toString, donc il y avait en fait une structure de diamant. Mais je pense que vous avez un point: une "jointure" sans structure en losange crée un problème similaire, et la plupart des langues traitent les deux cas de la même manière.
Daniel Lubarov

7

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

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

L'héritage est une astuce pour éviter une 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 épargne encore plus de travail par rapport à l'héritage simple, c'est toute la justification 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 plusieurs héritages à un programmeur C ++, et ce qui me vient à l’esprit est une question qui se pose lorsqu’une classe se retrouve avec deux copies d’une base via deux chemins d’héritage différents, et s’il faut utiliser virtualsur une classe de base et la confusion entourant l’appel des destructeurs. , 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, comme ces classes servent également d’espaces de noms lexicaux, vous devez importer des symboles de l’espace de noms B dans 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_decket graphicqui ont "une" drawméthode, que signifie-t-elle pour drawl'objet résultant? Un système d'objet qui n'a pas ce problème est celui de Common Lisp. Peut-être pas par coïncidence, 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é.


2
Votre exemple avec la méthode length () des conteneurs de liste et de chaîne est mauvais, car ces deux font des choses complètement différentes. L'héritage n'a pas pour but de réduire la répétition du code. Si vous souhaitez réduire la répétition de code, vous refactorisez les parties communes dans une nouvelle classe.
Février

1
@ BЈовић Les deux ne font pas du tout "complètement" des choses différentes.
Kaz

1
En combinant chaîne et liste , on peut voir beaucoup de répétitions. Utiliser l'héritage pour implémenter ces méthodes communes serait tout simplement une erreur.
BЈовић

1
@ BЈовић Il n'est pas dit dans la réponse qu'une chaîne et une liste doivent être liées par un héritage; plutôt l'inverse. Le comportement de pouvoir substituer une liste ou une chaîne dans une lengthopération est utile indépendamment du concept d'héritage (ce qui dans ce cas ne sert à rien: nous ne pourrons probablement rien obtenir en essayant de partager l'implémentation entre les deux méthodes de la lengthfonction). Il peut néanmoins y avoir un héritage abstrait: par exemple, list et string sont de type séquence (mais ne fournissent aucune implémentation).
Kaz

2
De plus, l'héritage est un moyen de refactoriser des pièces en une classe commune: la classe de base.
Kaz

1

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 données. perte de mémoire dans le cas suivant:

(Mon exemple n'est peut-être pas le meilleur, mais essayez de comprendre l'essentiel de l'espace mémoire multiple dans le même but, c'était la première chose qui me vint à l'esprit: P)

Concider un DDD où le chien de la classe s’étend de caninus et animal de compagnie, un caninus 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 aussi une autre variable à cette fin habituellement la même chose. name (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 de base), vous disposerez alors de deux espaces mémoire pour le paramètre exact. Dans le même but, pour éviter cela, vous devrez modifier votre compilateur afin de 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 au moment de la 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 le programmeur doit spécifier à la fin où se trouve cet espace mémoire auquel cette variable fait référence (et encore du code supplémentaire).

Croyez-moi, les gens qui appliquent cette pensée très durement à propos de tout cela, mais je suis heureux que vous ayez posé la question, votre perspective est celle qui change les paradigmes;), et notez ceci, je ne dis pas que c'est impossible (mais beaucoup de suppositions un compilateur multiphasé doit être implémenté, et un très complexe), je dis simplement 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 serai heureux de rejoindre votre équipe.


1
À quel moment un compilateur peut-il supposer que des variables du même nom issues de différentes classes parentes peuvent être combinées en toute sécurité? Quelle garantie avez-vous que caninus.diet et pet.diet servent réellement le même but? Que feriez-vous avec les fonctions / méthodes du même nom?
M.Mindor

hahaha je suis totalement d'accord avec vous @ Mr.Mindor c'est exactement ce que je dis dans ma réponse, le seul moyen qui me vienne à l'esprit de me rapprocher de cette approche est la programmation orientée prototype, mais je ne dis pas que c'est impossible , et c’est exactement la raison pour laquelle j’ai dit qu’il fallait faire beaucoup de suppositions au moment de la compilation et que quelques "données" / particularités supplémentaires devaient être écrites par le programmeur (néanmoins, c’est plus codant, ce qui était le problème original)
Ordiel

-1

Pendant un certain temps, je ne me suis jamais vraiment rendu compte à quel point certaines tâches de programmation étaient totalement différentes des autres - et à quel point il était utile que les langages et les modèles utilisés soient adaptés à l'espace du problème.

Lorsque vous travaillez seul ou principalement isolé sur du code que vous avez écrit, c'est un espace de problème complètement différent de l'héritage d'une base de code de 40 personnes en Inde qui y ont travaillé pendant un an avant de vous le remettre sans aucune aide transitoire.

Imaginez que vous veniez d'être embauché par la société de vos rêves et que vous héritiez ensuite d'une telle base de code. De plus, imaginez que les consultants aient appris sur l'héritage et l'héritage multiple (et qu'ils en aient été séduits) ... Pourriez-vous imaginer ce sur quoi vous pourriez travailler?

Lorsque vous héritez du code, la caractéristique la plus importante est sa compréhensibilité et le fait que les éléments sont isolés afin de pouvoir être travaillés indépendamment. Bien sûr, lorsque vous écrivez pour la première fois dans le code, des structures telles que l'héritage multiple peuvent vous faire économiser un peu de duplication et semblent correspondre à votre état d'esprit logique à ce moment-là, mais le gars suivant a juste plus de choses à démêler.

Chaque interconnexion dans votre code rend également plus difficile la compréhension et la modification de pièces de manière indépendante, encore plus avec un héritage multiple.

Lorsque vous travaillez au sein d’une équipe, vous souhaitez cibler le code le plus simple possible sans aucune logique redondante (c’est ce que signifie réellement DRY, non que vous ne deviez pas taper trop, mais que vous n’aviez jamais à changer de code 2 endroits pour résoudre un problème!)

Il existe des moyens plus simples d'obtenir du code DRY que l'héritage multiple. Par conséquent, l'inclure dans une langue ne vous ouvre que des problèmes insérés par d'autres personnes qui ne sont peut-être pas au même niveau de compréhension que vous. Il n’est même tentant que votre langue ne soit pas en mesure de vous offrir un moyen simple / moins complexe de conserver votre code au sec.


En outre, imaginez que les consultants aient appris - j'ai vu tellement de langages non-MI (par exemple, .net) où ils sont devenus fous avec les interfaces d'interface de dialogue et d'IoC basées sur une interface afin de transformer l'ensemble en un désordre impénétrable. MI ne fait pas qu'empirer les choses, probablement le rend meilleur.
gbjbaanb

@ gbjbaanb Vous avez raison, il est facile pour quelqu'un qui n'a pas été brûlé de gâcher n'importe quelle fonctionnalité. En fait, il est presque indispensable d'apprendre une fonctionnalité qui la gâche pour voir comment l'utiliser au mieux. Je suis un peu curieux parce que vous semblez dire que ne pas avoir plus de DI / IoC dans les forces MI que je n'ai pas vues - je ne penserais pas que le code de consultant que vous avez obtenu avec toutes les interfaces / ID de mauvaise qualité aurait été Mieux vaut aussi avoir tout un arbre de succession fou à plusieurs, mais ce pourrait être mon inexpérience avec MI. Avez-vous une page que je pourrais lire sur le remplacement de DI / IoC par MI?
Bill K

-3

Le principal argument contre l'héritage multiple est que certaines capacités utiles peuvent être fournies et certains axiomes utiles peuvent être conservés, dans un cadre qui le restreint sévèrement (*), mais ne peuvent pas être fournis et / ou conservés sans ces restrictions. Parmi eux:

  • La possibilité d'avoir plusieurs modules compilés séparément inclut des classes qui héritent des classes d'autres modules, et recompile un module contenant une classe de base sans avoir à recompiler chaque module qui hérite de cette classe de base.

  • Possibilité pour un type d'hériter d'une implémentation membre d'une classe parent sans que le type dérivé ait à le réimplémenter

  • L'axiome selon lequel une instance d'objet peut être convertie en amont ou en aval directement sur elle-même ou sur l'un de ses types de base, et ces retransmissions et downcasts, quelle que soit leur combinaison ou leur séquence, préservent toujours l'identité

  • L'axiome selon lequel si une classe dérivée substitue et enchaîne à un membre de la classe de base, le membre de base sera invoqué directement par le code de chaînage et ne sera invoqué nulle part ailleurs.

(*) Généralement, en exigeant que les types prenant en charge l'héritage multiple soient déclarés comme "interfaces" plutôt que comme classes, et en ne permettant pas aux interfaces de faire tout ce que les classes normales peuvent faire.

Si l'on souhaite autoriser l'héritage multiple généralisé, quelque chose d'autre doit céder la place. Si X et Y héritent tous deux de B, ils écrasent tous deux le même membre M et la chaîne vers l'implémentation de base, et si D hérite de X et Y mais ne remplace pas M, alors, étant donné une instance q de type D, que devrait (( B) q) .M () faire? Le fait de refuser une telle distribution violerait l'axiome selon lequel tout objet peut être converti en un type de base quelconque, mais tout comportement éventuel de la diffusion et de l'invocation du membre violerait l'axiome relatif à l'enchaînement de méthodes. On pourrait exiger que les classes ne soient chargées qu'en combinaison avec la version particulière d'une classe de base pour laquelle elles ont été compilées, mais cela est souvent délicat. Si le moteur d'exécution refuse de charger tout type dans lequel n'importe quel ancêtre peut être atteint par plusieurs routes, cela pourrait fonctionner, mais limiterait grandement l'utilité de l'héritage multiple. Autoriser les chemins d’héritage partagé uniquement en l’absence de conflits créerait des situations dans lesquelles une ancienne version de X serait compatible avec un ancien ou un nouveau Y, et un ancien Y serait compatible avec un ancien ou un nouveau X, mais les nouveaux X et nouveaux Y être compatibles, même si rien de tout ce qui, en soi, ne devrait être considéré comme un changement décisif.

Certains langages et structures autorisent l'héritage multiple, en partant du principe que les avantages de l'IM sont plus importants que ce qu'il faut abandonner pour le permettre. Cependant, les coûts de l'IM sont importants et, dans de nombreux cas, les interfaces fournissent 90% des avantages de l'IM pour une petite fraction du coût.


Les électeurs défavorisés prétendent-ils faire des commentaires? Je pense que ma réponse fournit des informations qui ne sont pas présentes dans d'autres langues, en ce qui concerne les avantages sémantiques pouvant être obtenus en interdisant l'héritage multiple. Le fait d'autoriser l'héritage multiple nécessiterait d'abandonner d'autres choses utiles constituerait une bonne raison pour quiconque valorisant ces autres choses davantage que l'héritage multiple pour s'opposer à MI.
Supercat

2
Les problèmes avec MI sont facilement résolus ou évités en utilisant les mêmes techniques que vous utiliseriez avec un héritage simple. Aucune des capacités / axiomes que vous avez mentionnés n'est intrinsèquement gênée par l'IM, à l'exception du deuxième ... et si vous héritez de deux classes offrant des comportements différents pour la même méthode, il vous appartient de résoudre ce problème de toute façon. Mais si vous vous trouvez dans l'obligation de le faire, vous utilisez probablement déjà mal l'héritage.
cHao

@cHao: Si l'on exige que les classes dérivées écrasent les méthodes parentes et non les enchaînent (même règle que les interfaces), on peut éviter les problèmes, mais c'est une limitation assez sévère. Si on utilise un modèle où FirstDerivedFoole remplacement Barn'est pas chaîné sur un non-protégé FirstDerivedFoo_Bar, et que SecondDerivedFoole remplacement est protégé par un non-virtuel SecondDerivedBar, et si le code qui veut accéder à la méthode de base utilise ces méthodes non virtuelles protégées, pourrait être une approche réalisable ...
Supercat

... (en évitant la syntaxe base.VirtualMethod), mais je ne connais aucun support linguistique pour faciliter une telle approche. Je souhaiterais qu'il existe un moyen propre de lier un bloc de code à une méthode protégée non virtuelle et à une méthode virtuelle avec la même signature sans nécessiter une méthode virtuelle "one-liner" (qui prend finalement beaucoup plus d'une ligne dans le code source).
Supercat

Il n'y a aucune exigence inhérente dans MI que les classes n'appellent pas les méthodes de base, et aucune raison particulière ne serait nécessaire. Il semble un peu étrange de blâmer MI, étant donné que ce problème affecte également le SI.
cHao
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.