it-swarm.com.de

Gibt es einen "echten" Grund, warum Mehrfachvererbung gehasst wird?

Ich mochte schon immer die Idee, Mehrfachvererbung in einer Sprache zu unterstützen. Meistens wird jedoch absichtlich darauf verzichtet, und der vermeintliche "Ersatz" sind Schnittstellen. Schnittstellen decken einfach nicht alle Gründe ab, die eine Mehrfachvererbung hat, und diese Einschränkung kann gelegentlich zu mehr Code auf dem Boilerplate führen.

Der einzige Grund, den ich jemals dafür gehört habe, ist das Diamantproblem mit Basisklassen. Das kann ich einfach nicht akzeptieren. Für mich kommt es sehr viel heraus wie: "Nun, es ist möglich, es zu vermasseln, also ist es automatisch eine schlechte Idee." Sie können jedoch alles in einer Programmiersprache vermasseln, und ich meine alles. Ich kann das einfach nicht ernst nehmen, zumindest nicht ohne eine gründlichere Erklärung.

Nur sich dieses Problems bewusst zu sein, macht 90% des Kampfes aus. Außerdem glaube ich, dass ich vor einigen Jahren von einer allgemeinen Umgehung mit einem "Hüllkurven" -Algorithmus oder ähnlichem gehört habe (klingelt das, jemand?).

In Bezug auf das Diamantproblem ist das einzige potenziell echte Problem, an das ich denken kann, wenn Sie versuchen, eine Bibliothek eines Drittanbieters zu verwenden, und nicht erkennen können, dass zwei scheinbar nicht verwandte Klassen in dieser Bibliothek eine gemeinsame Basisklasse haben, aber zusätzlich zu Dokumentation, eine einfache Sprachfunktion könnte beispielsweise erfordern, dass Sie ausdrücklich Ihre Absicht erklären, einen Diamanten zu erstellen, bevor dieser tatsächlich für Sie kompiliert wird. Mit einem solchen Merkmal ist jede Kreation eines Diamanten entweder beabsichtigt, rücksichtslos oder weil man sich dieser Gefahr nicht bewusst ist.

Damit alles gesagt wird ... Gibt es einen wirklichen Grund, warum die meisten Menschen Mehrfachvererbung hassen, oder ist alles nur ein Haufen Hysterie, der mehr Schaden verursacht als gut? Gibt es etwas, das ich hier nicht sehe? Vielen Dank.

Beispiel

Auto erweitert WheeledVehicle, KIASpectra erweitert Auto und Elektronik, KIASpectra enthält Radio. Warum enthält KIASpectra nicht Electronic?

  1. Weil es ist eine elektronische. Vererbung vs. Zusammensetzung sollte immer eine Is-a-Beziehung vs. eine Has-a-Beziehung sein.

  2. Weil es ist eine elektronische. Es gibt Drähte, Leiterplatten, Schalter usw. auf und ab.

  3. Weil es ist eine elektronische. Wenn Ihre Batterie im Winter leer ist, Sie haben genauso große Probleme, als ob plötzlich alle Räder verloren gegangen wären.

Warum nicht Schnittstellen verwenden? Nehmen Sie zum Beispiel # 3. Ich möchte dies nicht immer und immer wieder schreiben, und ich wirklich möchte auch keine bizarre Proxy-Helfer-Klasse erstellen, um dies zu tun:

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

(Wir werden nicht untersuchen, ob diese Implementierung gut oder schlecht ist.) Sie können sich vorstellen, wie es mehrere dieser mit Electronic verbundenen Funktionen gibt, die mit nichts in WheeledVehicle in Verbindung stehen, und umgekehrt -versa.

Ich war mir nicht sicher, ob ich mich auf dieses Beispiel festlegen sollte oder nicht, da dort Raum für Interpretationen besteht. Sie könnten auch in Bezug auf Flugzeugverlängerung Fahrzeug und FlyingObject und Vogelverlängerung Animal und FlyingObject oder in Bezug auf ein viel reineres Beispiel denken.

129
Panzercrisis

In vielen Fällen verwenden Menschen die Vererbung, um einer Klasse ein Merkmal zu verleihen. Denken Sie zum Beispiel an einen Pegasus. Bei mehrfacher Vererbung könnten Sie versucht sein zu sagen, dass der Pegasus Pferd und Vogel erweitert, weil Sie den Vogel als ein Tier mit Flügeln klassifiziert haben.

Vögel haben jedoch andere Eigenschaften, die Pegasi nicht hat. Zum Beispiel legen Vögel Eier, Pegasi haben Lebendgeburten. Wenn die Vererbung Ihr einziges Mittel ist, um gemeinsame Merkmale weiterzugeben, gibt es keine Möglichkeit, das Merkmal der Eiablage vom Pegasus auszuschließen.

Einige Sprachen haben sich dafür entschieden, Merkmale zu einem expliziten Konstrukt innerhalb der Sprache zu machen. Andere führen Sie sanft in diese Richtung, indem Sie MI aus der Sprache entfernen. Auf jeden Fall kann ich mir keinen einzigen Fall vorstellen, in dem ich dachte "Mann, ich brauche wirklich MI, um das richtig zu machen".

Lassen Sie uns auch diskutieren, was Vererbung WIRKLICH ist. Wenn Sie von einer Klasse erben, nehmen Sie eine Abhängigkeit von dieser Klasse an, müssen aber auch die Verträge unterstützen, die die Klasse sowohl implizit als auch explizit unterstützt.

Nehmen Sie das klassische Beispiel eines Quadrats, das von einem Rechteck erbt. Das Rechteck macht eine Eigenschaft für Länge und Breite sowie eine Methode getPerimeter und getArea verfügbar. Das Quadrat würde Länge und Breite überschreiben, so dass, wenn eines eingestellt ist, das andere auf getPerimeter und getArea eingestellt ist (2 * Länge + 2 * Breite für Umfang und Länge * Breite für Fläche).

Es gibt einen einzelnen Testfall, der unterbrochen wird, wenn Sie diese Implementierung eines Quadrats durch ein Rechteck ersetzen.

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

Es ist schwierig genug, mit einer einzigen Vererbungskette alles richtig zu machen. Es wird noch schlimmer, wenn Sie der Mischung eine weitere hinzufügen.


Die Fallstricke, die ich mit dem Pegasus in MI und den Rechteck/Quadrat-Beziehungen erwähnt habe, sind beide das Ergebnis eines unerfahrenen Entwurfs für Klassen. Grundsätzlich ist die Vermeidung von Mehrfachvererbung eine Möglichkeit, Anfängern dabei zu helfen, sich nicht in den Fuß zu schießen. Wie bei allen Designprinzipien können Sie durch Disziplin und Training rechtzeitig feststellen, wann es in Ordnung ist, sich von ihnen zu lösen. Siehe Dreyfus-Modell für den Erwerb von Fähigkeiten Auf Expertenebene geht Ihr intrinsisches Wissen über die Abhängigkeit von Maximen/Prinzipien hinaus. Sie können "fühlen", wenn eine Regel nicht gilt.

Und ich stimme zu, dass ich mit einem Beispiel aus der "realen Welt" betrogen habe, warum MI verpönt ist.

Schauen wir uns ein UI-Framework an. Schauen wir uns insbesondere einige Widgets an, die auf den ersten Blick so aussehen könnten, als wären sie einfach eine Kombination aus zwei anderen. Wie eine ComboBox. Eine ComboBox ist eine TextBox mit einer unterstützenden DropDownList. Das heißt, Ich kann einen Wert eingeben oder aus einer vorgegebenen Werteliste auswählen. Ein naiver Ansatz wäre, die ComboBox von TextBox und DropDownList zu erben.

Ihr Textfeld leitet seinen Wert jedoch von dem ab, was der Benutzer eingegeben hat. Während die DDL ihren Wert von dem erhält, was der Benutzer auswählt. Wer hat Präzedenzfall? Die DDL wurde möglicherweise entwickelt, um Eingaben zu überprüfen und abzulehnen, die nicht in der ursprünglichen Werteliste enthalten waren. Überschreiben wir diese Logik? Das heißt, wir müssen die interne Logik offenlegen, damit die Erben sie überschreiben können. Oder fügen Sie der Basisklasse, die nur vorhanden ist, Logik hinzu, um eine Unterklasse zu unterstützen (Verstoß gegen das Abhängigkeitsinversionsprinzip ).

Wenn Sie MI vermeiden, können Sie diese Gefahr insgesamt umgehen. Dies kann dazu führen, dass Sie allgemeine, wiederverwendbare Merkmale Ihrer UI-Widgets extrahieren, damit sie bei Bedarf angewendet werden können. Ein hervorragendes Beispiel hierfür ist WPF Attached Property , mit dem ein Framework-Element in WPF eine Eigenschaft bereitstellen kann, die ein anderes Framework-Element verwenden kann, ohne vom übergeordneten Framework-Element zu erben.

Ein Raster ist beispielsweise ein Layoutfenster in WPF und verfügt über Eigenschaften für Spalten und Zeilen, die angeben, wo ein untergeordnetes Element in der Anordnung des Rasters platziert werden soll. Wenn ich ohne angehängte Eigenschaften eine Schaltfläche in einem Raster anordnen möchte, muss die Schaltfläche vom Raster abgeleitet werden, damit sie auf die Eigenschaften "Spalte" und "Zeile" zugreifen kann.

Entwickler haben dieses Konzept weiterentwickelt und angehängte Eigenschaften verwendet, um das Verhalten zu komponieren (hier ist zum Beispiel mein Beitrag zum Erstellen eines sortierbare GridView mit angehängten Eigenschaften , der geschrieben wurde, bevor WPF ein DataGrid enthielt). Der Ansatz wurde als XAML-Entwurfsmuster mit dem Namen Attached Behaviors erkannt.

Hoffentlich lieferte dies ein wenig mehr Einblick, warum Multiple Inheritance normalerweise verpönt ist.

70
Michael Brown

Gibt es etwas, das ich hier nicht sehe?

Durch das Zulassen der Mehrfachvererbung werden die Regeln für Funktionsüberladungen und den virtuellen Versand sowie die Sprachimplementierung für Objektlayouts deutlich schwieriger. Diese wirken sich ziemlich stark auf Sprachdesigner/-implementierer aus und legen die ohnehin schon hohe Messlatte höher, um eine Sprache fertig zu stellen, stabil zu halten und zu übernehmen.

Ein weiteres häufiges Argument, das ich gesehen (und manchmal vorgebracht) habe, ist, dass Ihr Objekt durch zwei + Basisklassen fast immer gegen das Prinzip der Einzelverantwortung verstößt. Entweder sind die zwei + Basisklassen nette, in sich geschlossene Klassen mit eigener Verantwortung (die den Verstoß verursacht), oder es handelt sich um partielle/abstrakte Typen, die zusammenarbeiten, um eine einzige zusammenhängende Verantwortung zu übernehmen.

In diesem anderen Fall haben Sie 3 Szenarien:

  1. A weiß nichts über B - Großartig, Sie könnten die Klassen kombinieren, weil Sie Glück hatten.
  2. A kennt B - Warum hat A nicht einfach von B geerbt?
  3. A und B kennen sich - Warum hast du nicht nur eine Klasse gemacht? Welchen Vorteil hat es, diese Dinge so gekoppelt, aber teilweise austauschbar zu machen?

Persönlich denke ich, dass Mehrfachvererbung einen schlechten Ruf hat und dass ein gut gemachtes System der Komposition von Merkmalen wirklich mächtig/nützlich wäre ... aber es gibt viele Möglichkeiten, wie es schlecht implementiert werden kann, und viele Gründe dafür Keine gute Idee in einer Sprache wie C++.

[Bearbeiten] in Bezug auf Ihr Beispiel, das ist absurd. Ein Kia hat Elektronik. Es hat einen Motor. Ebenso ist es Elektronik haben eine Stromquelle, die zufällig eine Autobatterie ist. Vererbung, geschweige denn Mehrfachvererbung hat dort keinen Platz.

60
Telastyn

Der einzige Grund, warum es nicht erlaubt ist, ist, dass es Menschen leicht macht, sich in den Fuß zu schießen.

Was normalerweise in dieser Art von Diskussion folgt, sind Argumente, ob die Flexibilität der Werkzeuge wichtiger ist als die Sicherheit, nicht vom Fuß zu schießen. Es gibt keine entschieden richtige Antwort auf dieses Argument, da die Antwort wie die meisten anderen Dinge in der Programmierung vom Kontext abhängt.

Wenn Ihre Entwickler mit MI vertraut sind und MI im Kontext Ihrer Arbeit Sinn macht, werden Sie es in einer Sprache, die es nicht unterstützt, schmerzlich vermissen. Wenn das Team damit nicht vertraut ist oder es nicht wirklich benötigt wird und die Leute es "nur weil sie können" verwenden, ist das kontraproduktiv.

Aber nein, es gibt kein überzeugendes, absolut wahres Argument, das beweist, dass Mehrfachvererbung eine schlechte Idee ist.

EDIT

Die Antworten auf diese Frage scheinen einstimmig zu sein. Um der Anwalt des Teufels zu sein, werde ich ein gutes Beispiel für Mehrfachvererbung geben, bei dem es zu Hacks kommt, wenn man es nicht tut.

Angenommen, Sie entwerfen eine Kapitalmarktanwendung. Sie benötigen ein Datenmodell für Wertpapiere. Einige Wertpapiere sind Aktienprodukte (Aktien, Immobilieninvestmentfonds usw.), andere sind Schuldtitel (Anleihen, Unternehmensanleihen), andere sind Derivate (Optionen, Futures). Wenn Sie also MI vermeiden, erstellen Sie einen sehr klaren, einfachen Vererbungsbaum. Eine Aktie erbt das Eigenkapital, die Anleihe die Schulden. Bisher großartig, aber was ist mit Derivaten? Sie können auf aktienähnlichen Produkten oder belastungsähnlichen Produkten basieren? Ok, ich denke, wir werden unseren Erbbaum mehr verzweigen lassen. Beachten Sie, dass einige Derivate auf Aktienprodukten, Schuldtiteln oder beidem basieren. Unser Erbbaum wird also immer komplizierter. Dann kommt der Business Analyst und sagt Ihnen, dass wir jetzt indexierte Wertpapiere (Indexoptionen, zukünftige Indexoptionen) unterstützen. Und diese Dinge können auf Eigenkapital, Schulden oder Derivaten basieren. Das wird chaotisch! Leitet meine zukünftige Indexoption Aktien-> Aktie-> Option-> Index ab? Warum nicht Aktien-> Aktie-> Index-> ​​Option? Was ist, wenn ich eines Tages beides in meinem Code finde (Dies ist passiert; wahre Geschichte)?

Das Problem hierbei ist, dass diese Grundtypen in jeder Permutation gemischt werden können, die sich natürlich nicht voneinander ableitet. Die Objekte werden durch eine Beziehung definiert , daher macht Komposition überhaupt keinen Sinn. Mehrfachvererbung (oder das ähnliche Konzept von Mixins) ist hier die einzig logische Darstellung.

Die eigentliche Lösung für dieses Problem besteht darin, die Typen Equity, Debt, Derivative und Index unter Verwendung von Mehrfachvererbung zu definieren und zu mischen, um Ihr Datenmodell zu erstellen. Dadurch werden Objekte erstellt, die sowohl sinnvoll als auch für die Wiederverwendung von Code geeignet sind.

29
MrFox

Die anderen Antworten hier scheinen größtenteils in die Theorie zu kommen. Hier ist also ein konkretes Python Beispiel, vereinfacht, in das ich tatsächlich kopfüber hineingeschlagen bin, was einiges an Refactoring erforderte:

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 wurde unter der Annahme geschrieben, dass es eine eigene Implementierung von zeta() hat, was im Allgemeinen eine ziemlich gute Annahme ist. Eine Unterklasse sollte sie entsprechend überschreiben, damit sie das Richtige tut. Leider waren die Namen nur zufällig gleich - sie machten ziemlich unterschiedliche Dinge, aber Bar rief jetzt die Implementierung von Foo auf:

foozeta
---
barstuff
foozeta

Es ist ziemlich frustrierend, wenn keine Fehler ausgelöst werden, die Anwendung sich nur geringfügig falsch verhält und die Codeänderung, die sie verursacht hat (Erstellen von Bar.zeta), Nicht dort liegt, wo das Problem liegt.

11
Izkata

Ich würde argumentieren, dass es keine wirklichen Probleme mit MI in der richtigen Sprache gibt. Der Schlüssel besteht darin, Diamantstrukturen zuzulassen, aber zu erfordern, dass Subtypen ihre eigene Überschreibung bereitstellen, anstatt dass der Compiler eine der Implementierungen basierend auf einer Regel auswählt.

Ich mache das in Guava , einer Sprache, an der ich arbeite. Ein Merkmal von Guava ist, dass wir die Implementierung einer Methode durch einen bestimmten Supertyp aufrufen können. Es ist also einfach anzugeben, welche Supertyp-Implementierung ohne spezielle Syntax "vererbt" werden soll:

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);
  }
}

Wenn wir OrderedSet kein eigenes toString geben würden, würden wir einen Kompilierungsfehler erhalten. Keine Überraschungen.

Ich finde MI besonders nützlich bei Sammlungen. Zum Beispiel möchte ich einen RandomlyEnumerableSequence -Typ verwenden, um zu vermeiden, dass getEnumerator für Arrays, Deques usw. deklariert wird:

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.
}

Wenn wir keinen MI hätten, könnten wir ein RandomAccessEnumerator für mehrere Sammlungen schreiben, aber wenn wir eine kurze getEnumerator -Methode schreiben müssen, wird immer noch Boilerplate hinzugefügt.

In ähnlicher Weise ist MI nützlich, um Standardimplementierungen von equals, hashCode und toString für Sammlungen zu erben.

9
Daniel Lubarov

Vererbung, mehrfach oder auf andere Weise, ist nicht so wichtig. Wenn zwei Objekte unterschiedlichen Typs austauschbar sind, ist dies wichtig, auch wenn sie nicht durch Vererbung verbunden sind.

Eine verknüpfte Liste und eine Zeichenfolge haben wenig gemeinsam und müssen nicht durch Vererbung verknüpft werden. Es ist jedoch nützlich, wenn ich eine length -Funktion verwenden kann, um die Anzahl der Elemente in einem der beiden Elemente zu ermitteln.

Vererbung ist ein Trick, um eine wiederholte Implementierung von Code zu vermeiden. Wenn die Vererbung Ihnen Arbeit erspart und die Mehrfachvererbung Ihnen im Vergleich zur Einzelvererbung noch mehr Arbeit erspart, ist dies die erforderliche Rechtfertigung.

Ich vermute, dass einige Sprachen die Mehrfachvererbung nicht sehr gut implementieren, und für die Praktiker dieser Sprachen bedeutet dies Mehrfachvererbung. Erwähnen Sie die Mehrfachvererbung für einen C++ - Programmierer, und Sie denken an Probleme, wenn eine Klasse über zwei verschiedene Vererbungspfade zwei Kopien einer Basis erhält und ob virtual für eine Basisklasse verwendet werden soll, und Verwirrung darüber, wie Destruktoren genannt werden, und so weiter.

In vielen Sprachen wird die Vererbung von Klassen mit der Vererbung von Symbolen in Verbindung gebracht. Wenn Sie eine Klasse D von einer Klasse B ableiten, erstellen Sie nicht nur eine Typbeziehung, sondern da diese Klassen auch als lexikalische Namespaces dienen, beschäftigen Sie sich zusätzlich mit dem Import von Symbolen aus dem B-Namespace in den D-Namespace die Semantik dessen, was mit den Typen B und D selbst passiert. Mehrfachvererbung bringt daher Probleme mit Symbolkonflikten mit sich. Wenn wir von card_deck Und graphic erben, die beide eine draw -Methode "haben", was bedeutet es, draw das resultierende Objekt zu _? Ein Objektsystem, das dieses Problem nicht hat, ist das in Common LISP. Vielleicht nicht zufällig wird die Mehrfachvererbung in LISP-Programmen verwendet.

Schlecht implementiert, unpraktisch alles (wie Mehrfachvererbung) sollte gehasst werden.

7
Kaz

Soweit ich das beurteilen kann, besteht ein Teil des Problems (abgesehen davon, dass Ihr Design etwas schwieriger zu verstehen ist (dennoch einfacher zu codieren)) darin, dass der Compiler genügend Speicherplatz für Ihre Klassendaten spart, was eine große Menge an Speicherplatz ermöglicht Speicherverschwendung im folgenden Fall:

(Mein Beispiel ist vielleicht nicht das beste, aber versuchen Sie, das Wesentliche über mehrere Speicherplätze für denselben Zweck zu erfahren. Es war das erste, was mir in den Sinn kam: P)

Betrachtet man eine DDD, bei der sich der Klassenhund von Caninus und Haustier erstreckt, so hat ein Caninus eine Variable, die die Menge an Futter angibt, die er essen sollte (eine ganze Zahl), unter dem Namen dietKg, aber ein Haustier hat zu diesem Zweck auch eine andere Variable, normalerweise unter derselben Name (es sei denn, Sie legen einen anderen Variablennamen fest, dann müssen Sie zusätzlichen Code codieren, was das ursprüngliche Problem war, das Sie vermeiden wollten, um die Integrität von Mundvariablen zu handhaben und beizubehalten), dann haben Sie zwei Speicherplätze für die genaue Um dies zu vermeiden, müssen Sie Ihren Compiler ändern, um diesen Namen unter demselben Namespace zu erkennen, und diesen Daten nur einen einzigen Speicherplatz zuweisen, der in der Kompilierungszeit leider nicht bestimmt werden kann.

Sie können natürlich eine Sprache entwerfen, um anzugeben, dass für eine solche Variable möglicherweise bereits ein Speicherplatz an einer anderen Stelle definiert ist. Am Ende sollte der Programmierer jedoch angeben, wo sich der Speicherplatz befindet, auf den sich diese Variable bezieht (und erneut zusätzlichen Code).

Vertrauen Sie mir, die Leute, die diesen Gedanken wirklich hart umsetzen, aber ich bin froh, dass Sie gefragt haben, Ihre freundliche Perspektive ist die, die die Paradigmen ändert;), und wenn ich das bedenke, sage ich nicht, dass es unmöglich ist (aber viele Annahmen und Es muss ein mehrphasiger Compiler implementiert werden, und ein wirklich komplexer. Ich sage nur, dass er noch nicht existiert. Wenn Sie ein Projekt für Ihren eigenen Compiler starten, der "dies" (Mehrfachvererbung) kann, lassen Sie es mich bitte wissen Ich freue mich, Ihrem Team beizutreten.

1
Ordiel