Gibt es einen „echten“ Grund, warum Mehrfachvererbung gehasst wird?

123

Die Idee, mehrere Vererbungen in einer Sprache zu unterstützen, hat mir immer gefallen. Meistens wird jedoch absichtlich darauf verzichtet, und der vermeintliche "Ersatz" sind Schnittstellen. Interfaces decken einfach nicht den gleichen Grund ab, den Mehrfachvererbung hat, und diese Einschränkung kann gelegentlich zu mehr Boilerplate-Code 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 ist es eine Menge, wie: "Nun, es ist möglich , es zu vermasseln, also ist es automatisch eine schlechte Idee." Sie können 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, ist 90% der Schlacht. Ich glaube, ich habe vor einigen Jahren von einem allgemeinen Workaround mit einem "Envelope" -Algorithmus oder ähnlichem gehört (läutet das eine Glocke?).

In Bezug auf das Diamantenproblem ist das einzige potenziell echte Problem, das mir einfällt, wenn Sie versuchen, eine Bibliothek eines Drittanbieters zu verwenden, und nicht sehen können, dass zwei scheinbar nicht miteinander verbundene Klassen in dieser Bibliothek eine gemeinsame Basisklasse haben, aber zusätzlich dazu Die Dokumentation, eine einfache Sprachfunktion, könnte beispielsweise erfordern, dass Sie ausdrücklich Ihre Absicht erklären, einen Diamanten zu erstellen, bevor er tatsächlich einen für Sie kompiliert. Mit einem solchen Merkmal ist jede Kreation eines Diamanten entweder beabsichtigt, rücksichtslos oder weil man sich dieser Fallstricke nicht bewusst ist.

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

Beispiel

Auto erweitert WheeledVehicle, KIASpectra erweitert Auto und Electronic, KIASpectra enthält Radio. Warum enthält KIASpectra kein elektronisches Produkt?

  1. Denn es ist ein Electronic. Vererbung vs. Komposition sollte immer eine Beziehung sein vs. eine Beziehung.

  2. Denn es ist ein Electronic. Es gibt Drähte, Leiterplatten, Schalter usw., die das Ding auf und ab bewegen.

  3. Denn es ist ein Electronic. Wenn Ihre Batterie im Winter leer ist, haben Sie genauso große Probleme, als wären plötzlich alle Räder verschwunden.

Warum keine Schnittstellen verwenden? Nehmen wir zum Beispiel # 3. Ich will nicht , um darüber zu schreiben und immer wieder, und ich wirklich nicht will , etwas bizarr Proxy Helfer Klasse erstellen , dies zu tun , entweder:

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

(Wir werden uns nicht damit befassen, ob diese Implementierung gut oder schlecht ist.) Sie können sich vorstellen, wie viele dieser Funktionen in Zusammenhang mit Electronic stehen, die nichts mit WheeledVehicle zu tun haben, und umgekehrt.

Ich war mir nicht sicher, ob ich mich mit diesem Beispiel abfinden sollte oder nicht, da es dort Raum für Interpretationen gibt. Sie könnten auch in Bezug auf die Flugzeugerweiterung von Fahrzeug und FlyingObject und die Vogelerweiterung von Animal und FlyingObject denken, oder in Bezug auf ein viel reineres Beispiel.

Panzerkrise
quelle
24
es fördert auch die Vererbung über die Zusammensetzung ... (wenn es umgekehrt sein sollte)
Ratschenfreak
34
"Sie können es vermasseln" ist ein durchaus berechtigter Grund, ein Feature zu entfernen. Modernes Sprachdesign ist in diesem Punkt etwas zersplittert, aber Sie können Sprachen im Allgemeinen in "Kraft aus Einschränkung" und "Kraft aus Flexibilität" einteilen. Keiner von beiden ist "richtig", glaube ich nicht, beide haben starke Punkte zu machen. MI ist eines der Dinge, die für das Böse verwendet werden, und daher wird es von restriktiven Sprachen entfernt. Flexible nicht, weil "meistens" nicht "buchstäblich immer" ist. Das heißt, Mixins / Traits sind eine bessere Lösung für den allgemeinen Fall, denke ich.
Phoshi
8
Es gibt sicherere Alternativen zur Mehrfachvererbung in mehreren Sprachen, die über Schnittstellen hinausgehen. Schauen Sie sich Scala's an Traits- sie wirken wie Schnittstellen mit optionaler Implementierung, haben jedoch einige Einschränkungen, die das Auftreten von Problemen wie dem Diamantproblem verhindern.
KChaloux
5
Siehe auch diese früher geschlossene Frage: programmers.stackexchange.com/questions/122480/…
Doc Brown
19
KiaSpectraist nicht ein Electronic ; es hat Elektronik, und kann ein ElectronicCar(was sich verlängern würde Car...)
Brian S

Antworten:

68

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 Lebendgeburt. Wenn die Vererbung Ihr einziges Mittel ist, um gemeinsame Merkmale weiterzugeben, können Sie das Merkmal der Eiablage nicht vom Pegasus ausschließen.

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

Besprechen wir auch, 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 von dieser Klasse unterstützt werden, sowohl implizit als auch explizit.

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 festgelegt wird, das andere so festgelegt wird, dass es mit getPerimeter und getArea übereinstimmt (2 * Länge + 2 * Breite für Umfang und Länge * Breite für Fläche).

Es gibt einen einzelnen Testfall, der fehlschlägt, 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 dem Mix einen weiteren hinzufügen.


Die Fallstricke, die ich im Zusammenhang mit dem Pegasus in MI und den Beziehungen Rechteck / Quadrat erwähnt habe, sind beide das Ergebnis eines unerfahrenen Entwurfs für Klassen. Das Vermeiden von Mehrfachvererbungen ist eine Möglichkeit, damit sich Entwickler nicht selbst in den Fuß schießen müssen. Wie bei allen Konstruktionsprinzipien können Sie durch die darauf basierende Disziplin und Schulung rechtzeitig erkennen, wann es in Ordnung ist, sich von ihnen zu lösen. Sehen Sie sich das Dreyfus-Modell für den Erwerb von Fähigkeiten an. Auf Expertenebene übersteigt Ihr intrinsisches Wissen die Abhängigkeit von Maximen / Prinzipien. Sie können fühlen, wenn eine Regel nicht zutrifft.

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

Schauen wir uns ein UI-Framework an. Lassen Sie uns speziell einige Widgets betrachten, 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. Dh 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.

Ihre Textbox leitet ihren 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 einen 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 bedeutet, dass wir die interne Logik offen legen müssen, damit Erben überschreiben können. Oder, schlimmer noch, fügen Sie der Basisklasse Logik hinzu, die nur vorhanden ist, um eine Unterklasse zu unterstützen (was gegen das Abhängigkeitsinversionsprinzip verstößt ).

Wenn Sie MI vermeiden, können Sie diese Gefahr ganz umgehen. Dies kann dazu führen, dass Sie häufig wiederverwendbare Eigenschaften Ihrer UI-Widgets extrahieren, damit diese bei Bedarf angewendet werden können. Ein hervorragendes Beispiel hierfür ist die angehängte WPF-Eigenschaft , mit der 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 Layoutbereich in WPF und verfügt über angehängte Spalten- und Zeileneigenschaften, die angeben, wo ein untergeordnetes Element in der Rasteranordnung 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 Spalten- und Zeileneigenschaften zugreifen kann.

Die Entwickler haben dieses Konzept weiterentwickelt und angehängte Eigenschaften als Methode zum Komponieren des Verhaltens verwendet (hier ist beispielsweise mein Beitrag zum Erstellen einer sortierbaren GridView mit angehängten Eigenschaften, die geschrieben wurden, bevor WPF ein DataGrid enthielt). Der Ansatz wurde als XAML-Entwurfsmuster namens Attached Behaviors erkannt .

Hoffentlich lieferte dies ein wenig mehr Aufschluss darüber, warum Mehrfachvererbung in der Regel verpönt ist.

Michael Brown
quelle
14
"Mann, ich brauche wirklich MI, um das richtig zu machen" - siehe meine Antwort für ein Beispiel. Auf den Punkt gebracht, sind orthogonale Konzepte, die eine Is-A-Beziehung erfüllen, für MI sinnvoll. Sie sind sehr selten, aber sie existieren.
MrFox
13
Mann, ich hasse dieses quadratische Rechteckbeispiel. Es gibt viele aufschlussreichere Beispiele, die keine Veränderbarkeit erfordern.
Asmeurer
9
Ich finde es toll, dass die Antwort auf "ein echtes Beispiel" darin bestand, über eine fiktive Kreatur zu diskutieren. Ausgezeichnete Ironie +1
Jjathman
3
Sie sagen also, dass Mehrfachvererbung schlecht ist, weil Vererbung schlecht ist (in Ihren Beispielen geht es nur um die Vererbung einzelner Schnittstellen). Aus diesem Grund sollte eine Sprache allein auf der Grundlage dieser Antwort entweder keine Vererbung und keine Schnittstellen mehr aufweisen oder eine Mehrfachvererbung aufweisen.
Strg-Alt-Delor
12
Ich glaube nicht, dass diese Beispiele wirklich funktionieren. Niemand sagt, ein Pegasus sei ein Vogel und ein Pferd. Sie sagen, es sei ein WingedAnimal und ein HoofedAnimal. Das Quadrat- und Rechteck-Beispiel ist etwas sinnvoller, weil Sie sich mit einem Objekt konfrontiert sehen, das sich anders verhält, als Sie aufgrund seiner behaupteten Definition denken würden. Ich denke jedoch, dass dies der Fehler des Programmierers ist, wenn er dies nicht bemerkt (wenn sich dies tatsächlich als Problem herausgestellt hat).
Richard
60

Gibt es etwas, das ich hier nicht sehe?

Durch das Ermöglichen der Mehrfachvererbung werden die Regeln für Funktionsüberladungen und den virtuellen Versand sowie die Implementierung von Sprachen für Objektlayouts deutlich schwieriger. Diese beeinflussen Sprachdesigner / -implementierer ziemlich stark und legen die ohnehin schon hohe Messlatte, um eine Sprache fertigzustellen, die stabil und adoptiert ist.

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

In diesem anderen Fall haben Sie 3 Szenarien:

  1. A weiß nichts über B - Großartig, Sie konnten die Klassen kombinieren, weil Sie Glück hatten.
  2. A weiß über B Bescheid - Warum hat A nicht einfach von B geerbt?
  3. A und B wissen voneinander - Warum hast du nicht nur eine Klasse gemacht? Welchen Nutzen bringt 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 für die 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 ++.

[edit] in Bezug auf dein Beispiel ist das absurd. Ein Kia hat Elektronik. Es hat einen Motor. Ebenso verfügt die Elektronik über eine Stromquelle, bei der es sich zufällig um eine Autobatterie handelt. Vererbung, geschweige denn Mehrfachvererbung hat dort keinen Platz.

Telastyn
quelle
8
Eine weitere interessante Frage ist, wie Basisklassen und ihre Basisklassen initialisiert werden. Kapselung> Vererbung.
Jozefg
"Ein gut gemachtes System für die Komposition von Merkmalen wäre wirklich mächtig / nützlich ..." - Diese sind als Mixins bekannt und sie sind mächtig / nützlich. Mixins können mit MI implementiert werden, für Mixins ist jedoch keine Mehrfachvererbung erforderlich. Einige Sprachen unterstützen von Natur aus Mixins ohne MI.
BlueRaja - Danny Pflughoeft
Insbesondere für Java haben wir bereits mehrere Schnittstellen und INVOKEINTERFACE, sodass MI keine offensichtlichen Auswirkungen auf die Leistung hat.
Daniel Lubarov
Tetastyn, ich stimme zu, dass das Beispiel schlecht war und nicht seiner Frage entsprach. Vererbung gilt nicht für Adjektive (elektronisch), sondern für Substantive (Fahrzeug).
Richard
29

Der einzige Grund, warum dies nicht erlaubt ist, besteht darin, dass es den Menschen leicht gemacht wird, 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. Auf dieses Argument gibt es keine eindeutig richtige Antwort, da die Antwort wie bei den meisten anderen Dingen 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, sehr vermissen. Wenn sich das Team damit nicht wohl fühlt oder wenn es keinen wirklichen Bedarf gibt und die Leute es "nur weil sie können" verwenden, ist das kontraproduktiv.

Aber nein, es gibt kein überzeugendes, absolut wahres Argument, das die Mehrfachvererbung als schlechte Idee erweist.

BEARBEITEN

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 der es nicht zu Hacks kommt.

Angenommen, Sie entwerfen eine Kapitalmarktanwendung. Sie benötigen ein Datenmodell für Wertpapiere. Einige Wertpapiere sind Aktienprodukte (Aktien, Immobilienfonds usw.), andere sind Schuldtitel (Anleihen, Unternehmensanleihen), andere sind Derivate (Optionen, Futures). Wenn Sie MI vermeiden, erstellen Sie einen sehr übersichtlichen, einfachen Vererbungsbaum. Eine Aktie erbt Eigenkapital, eine Anleihe Schulden. Bisher großartig, aber was ist mit Derivaten? Sie können auf Equity-ähnlichen Produkten oder Debit-ähnlichen Produkten basieren? Ok, ich denke, wir werden unseren Vererbungsbaumzweig weiter ausbauen. Denken Sie daran, dass einige Derivate auf Aktienprodukten, Schuldtiteln oder beiden basieren. Unser Erbschaftsbaum wird also immer komplizierter. Dann kommt der Business Analyst und teilt Ihnen mit, dass wir jetzt indexierte Wertpapiere (Indexoptionen, Index-Future-Optionen) unterstützen. Und diese Dinge können auf Eigenkapital, Schulden oder Derivaten basieren. Das wird unordentlich! Leiten meine zukünftigen Indexoptionen Aktien-> Aktien-> Optionen-> Index ab? Warum nicht Aktien-> Aktien-> Index-> ​​Optionen? Was ist, wenn ich eines Tages beide in meinem Code finde?

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

Die eigentliche Lösung für dieses Problem besteht darin, die Datentypen Equity, Debt, Derivative, Index unter Verwendung von Mehrfachvererbung zu definieren und zu mischen, um Ihr Datenmodell zu erstellen. Dadurch werden Objekte erstellt, die beide sinnvoll sind und sich leicht zur Wiederverwendung von Code eignen.

MrFox
quelle
27
Ich habe im Finanzwesen gearbeitet. Es gibt einen Grund, warum funktionale Programmierung in Finanzsoftware OO vorgezogen wird. Und Sie haben darauf hingewiesen. MI behebt dieses Problem nicht, es verschärft es. Glauben Sie mir, ich kann Ihnen nicht sagen, wie oft ich gehört habe: "Es ist einfach so ... außer", wenn ich mit Finanzkunden zu tun habe. Das außer wird der Fluch meiner Existenz.
Michael Brown
8
Das scheint mir mit Schnittstellen sehr lösbar zu sein ... in der Tat scheint es für Schnittstellen besser geeignet zu sein als alles andere. Equityund Debtbeide implementieren ISecurity. Derivativehat eine ISecurityEigenschaft. Es kann sich um eine ISecuritygegebenenfalls (ich weiß nicht, Finanzen). IndexedSecuritiesEnthält wieder eine Interface-Eigenschaft, die auf die Typen angewendet wird, auf denen sie basieren dürfen. Wenn sie alle sind ISecurity, dann haben sie alle ISecurityEigenschaften und können beliebig verschachtelt werden ...
Bobson
11
@ Bobson das Problem kommt, dass Sie die Schnittstelle für jeden Implementierer neu implementieren müssen, und in vielen Fällen ist es genau das gleiche. Sie können die Delegierung / Komposition verwenden, verlieren dann jedoch den Zugriff auf private Mitglieder. Und da Java keine Delegierten / Lambdas hat, gibt es buchstäblich keine gute Möglichkeit, dies zu tun. Außer vielleicht mit einem Aspect-Tool wie Aspect4J
Michael Brown
10
@ Bobson genau das, was Mike Brown gesagt hat. Ja, Sie können eine Lösung mit Schnittstellen entwerfen, diese ist jedoch umständlich. Ihre Intuition, Interfaces zu verwenden, ist jedoch sehr korrekt, es ist ein versteckter Wunsch nach Mixins / Mehrfachvererbung :).
MrFox
11
Dies trifft auf eine Kuriosität zu, bei der OO-Programmierer Vererbung bevorzugen und Delegation häufig nicht als gültigen OO-Ansatz akzeptieren. Dies führt in der Regel zu einer schlankeren, wartbareren und erweiterbareren Lösung. Nachdem ich in den Bereichen Finanzen und Gesundheitswesen gearbeitet habe, habe ich gesehen, dass das unüberschaubare Durcheinander, das durch die MI verursacht werden kann, besonders dann entstehen kann, wenn sich die Steuer- und Gesundheitsgesetze von Jahr zu Jahr ändern und eine Definition eines Objekts LETZTES Jahr für DIESES Jahr ungültig ist (und dennoch die Funktion eines Jahres erfüllen muss und muss austauschbar sein). Komposition liefert schlankeren Code, der einfacher zu testen ist und mit der Zeit weniger kostet.
Shaun Wilson
11

Die anderen Antworten scheinen sich hauptsächlich mit der Theorie zu befassen. Hier ist ein konkretes Python-Beispiel, das vereinfacht dargestellt ist und in das ich kopfüber hineingebrochen bin.

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()

Barwurde unter der Annahme geschrieben, dass es eine eigene Implementierung von hat zeta(), 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 taten etwas anderes, Barriefen aber jetzt die FooUmsetzung auf:

foozeta
---
barstuff
foozeta

Es ist ziemlich frustrierend, wenn keine Fehler ausgegeben werden, die Anwendung sich nur leicht falsch Bar.zetaverhält und die Codeänderung, die sie verursacht hat, nicht das Problem zu sein scheint.

Izkata
quelle
Wie wird das Diamond-Problem durch Anrufe an vermieden super()?
Panzercrisis
@ Panzercrisis Sorry, vergiss diesen Teil. Ich habe mich falsch erinnert, auf welches Problem sich das "Diamantproblem" normalerweise bezieht - Python hatte ein separates Problem, das durch die rautenförmige Vererbung verursacht wurde, super()die sich
herumgesprochen hat
5
Ich bin froh, dass C ++ 's MI viel besser ist. Der obige Fehler ist einfach nicht möglich. Sie müssen es manuell disambiguieren, bevor der Code überhaupt kompiliert wird.
Thomas Eding
2
Das Ändern der Reihenfolge der Vererbung in Bangin Bar, Foowürde das Problem ebenfalls beheben - aber Ihr Punkt ist gut, +1.
Sean Vieira
1
@ThomasEding Sie können immer noch manuell disambiguate: Bar.zeta(self).
Tyler Crompton
9

Ich würde argumentieren, dass es mit MI in der richtigen Sprache keine wirklichen Probleme gibt . Der Schlüssel ist, Diamantstrukturen zuzulassen, aber zu verlangen, dass Subtypen ihre eigene Außerkraftsetzung 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. So ist es einfach, ohne spezielle Syntax anzugeben, welche Supertype-Implementierung "geerbt" 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 kein OrderedSeteigenes toStringangeben, erhalten wir einen Kompilierungsfehler. Keine Überraschungen.

Ich finde MI besonders nützlich für Sammlungen. Ich verwende beispielsweise gerne einen RandomlyEnumerableSequenceTyp, um zu vermeiden, getEnumeratordass Arrays, Deques usw. deklariert werden :

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 kein MI hätten, könnten wir ein RandomAccessEnumeratorfür mehrere Sammlungen schreiben, aber wenn wir eine kurze getEnumeratorMethode schreiben müssten, fügt das immer noch Boilerplate hinzu.

In ähnlicher Weise ist MI nützlich für die Vererbungsstandardimplementierungen von equals, hashCodeund toStringfür Sammlungen.

Daniel Lubarov
quelle
1
@AndyzSmith, ich verstehe Ihren Standpunkt, aber nicht alle Vererbungskonflikte sind tatsächliche semantische Probleme. Betrachten Sie mein Beispiel: Keiner der Typen macht Versprechungen über toStringdas Verhalten des Benutzers, sodass das Überschreiben des Verhaltens nicht gegen das Substitutionsprinzip verstößt. Manchmal kommt es auch zu "Konflikten" zwischen Methoden, die dasselbe Verhalten, jedoch unterschiedliche Algorithmen aufweisen, insbesondere bei Auflistungen.
Daniel Lubarov
1
@Asmageddon stimmte zu, meine Beispiele betreffen nur die Funktionalität. Was sehen Sie als die Vorteile von Mixins? Zumindest in Scala handelt es sich bei Merkmalen im Wesentlichen nur um abstrakte Klassen, die MI zulassen - sie können weiterhin zur Identifizierung verwendet werden und führen immer noch zum Diamantenproblem. Gibt es andere Sprachen, in denen Mixins interessantere Unterschiede aufweisen?
Daniel Lubarov
1
Technisch abstrakte Klassen können als Interfaces verwendet werden. Der Vorteil besteht einfach darin, dass das Konstrukt selbst ein "Verwendungstipp" ist, was bedeutet, dass ich weiß, was ich zu erwarten habe, wenn ich den Code sehe. Das heißt, ich programmiere derzeit in Lua, das kein inhärentes OOP-Modell hat. In vorhandenen Klassensystemen ist es normalerweise möglich, Tabellen als Mixins einzuschließen, und in dem Programm, das ich programmiere, werden Klassen als Mixins verwendet. Der einzige Unterschied besteht darin, dass Sie nur (einzelne) Vererbung für die Identität, Mixins für die Funktionalität (ohne Identitätsinformationen) und Schnittstellen für weitere Überprüfungen verwenden können. Vielleicht ist es nicht irgendwie besser, aber es macht Sinn für mich.
Llamageddon
2
Warum nennst du das Ordered extends Set and Sequencea Diamond? Es ist nur ein Join. Es fehlt der hatScheitelpunkt. Warum nennst du es einen Diamanten? Ich habe hier gefragt , aber es scheint eine Tabu-Frage. Woher wussten Sie, dass Sie diese dreieckige Struktur eher als Diamant als als Verbinden bezeichnen müssen?
Val
1
@RecognizeEvilasWaste diese Sprache hat einen impliziten TopTyp, der deklariert toString, also gab es tatsächlich eine Diamantstruktur. Aber ich denke, Sie haben einen Punkt - ein "Join" ohne eine Raute-Struktur erzeugt ein ähnliches Problem, und die meisten Sprachen behandeln beide Fälle auf die gleiche Weise.
Daniel Lubarov
7

Vererbung, mehrfach oder auf andere Weise, ist nicht so wichtig. Wenn zwei Objekte unterschiedlichen Typs substituierbar 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 mithilfe einer lengthFunktion die Anzahl der Elemente in einem der beiden Elemente ermitteln kann.

Vererbung ist ein Trick, um die wiederholte Implementierung von Code zu vermeiden. Wenn die Vererbung Ihre Arbeit spart und die Mehrfachvererbung im Vergleich zur Einzelvererbung noch mehr Arbeit spart, ist dies die einzige Rechtfertigung, die erforderlich ist.

Ich vermute, dass einige Sprachen Mehrfachvererbung nicht sehr gut implementieren, und für die Praktiker dieser Sprachen bedeutet Mehrfachvererbung dies. Erwähnen Sie die Mehrfachvererbung für einen C ++ - Programmierer, und was Ihnen einfällt, sind Probleme, wenn eine Klasse über zwei verschiedene Vererbungspfade zwei Kopien einer Basis erhält und ob sie virtualfür eine Basisklasse verwendet werden soll, und Unklarheiten darüber, wie Destruktoren aufgerufen werden , und so weiter.

In vielen Sprachen ist die Vererbung von Klassen mit der Vererbung von Symbolen verbunden. Wenn Sie eine Klasse D von einer Klasse B ableiten, erstellen Sie nicht nur eine Typbeziehung. Da diese Klassen auch als lexikalische Namespaces dienen, müssen Sie zusätzlich auch Symbole aus dem B-Namespace in den D-Namespace importieren die Semantik dessen, was mit den Typen B und D selbst geschieht. Die Mehrfachvererbung führt daher zu Problemen mit Symbolkonflikten. Wenn wir von card_deckund erben graphic, die beide eine drawMethode "haben" , was bedeutet das für drawdas resultierende Objekt? Ein Objektsystem, das dieses Problem nicht hat, ist das in Common Lisp. Vielleicht nicht zufällig wird Mehrfachvererbung in Lisp-Programmen verwendet.

Schlecht umgesetzt, unbequem etwas (wie Mehrfachvererbung) sollten gehasst werden.

Kaz
quelle
2
Ihr Beispiel mit der length () -Methode für Listen- und String-Container ist schlecht, da diese beiden völlig verschiedene Dinge tun. Der Zweck der Vererbung besteht nicht darin, die Codewiederholung zu reduzieren. Wenn Sie die Wiederholung von Code reduzieren möchten, ordnen Sie gemeinsame Teile einer neuen Klasse zu.
BЈови at
1
@ BЈовић Die beiden machen gar keine "ganz" unterschiedlichen Sachen.
Kaz
1
Wenn man String und Liste vergleicht , sieht man viele Wiederholungen. Die Verwendung der Vererbung zur Implementierung dieser allgemeinen Methoden wäre einfach falsch.
Bћовић
1
@ BЈовић In der Antwort steht nicht, dass ein String und eine Liste durch Vererbung verknüpft werden sollen. ganz im Gegenteil. Das Verhalten, eine Liste oder einen String in einer lengthOperation ersetzen zu können, ist unabhängig vom Konzept der Vererbung nützlich (was in diesem Fall nicht hilft: Es ist unwahrscheinlich, dass wir irgendetwas erreichen können, wenn wir versuchen, die Implementierung zwischen beiden zu teilen die beiden Methoden der lengthFunktion). Es könnte dennoch zu einer abstrakten Vererbung kommen: z. B. sind sowohl Liste als auch Zeichenfolge von der Typsequenz (die jedoch keine Implementierung bietet).
Kaz
2
Vererbung ist auch eine Möglichkeit, Teile in eine gemeinsame Klasse umzugestalten: die Basisklasse.
Kaz
1

Soweit ich das beurteilen kann, ist ein Teil des Problems (abgesehen davon, dass Ihr Design ein bisschen schwieriger zu verstehen ist (und dennoch einfacher zu codieren ist)), dass der Compiler genug Platz für Ihre Klassendaten spart, was eine enorme Menge von zulässt Speicherverschwendung in folgendem Fall:

(Mein Beispiel ist vielleicht nicht das beste, aber ich versuche, das Wesentliche über mehrere Speicherbereiche zum selben Zweck herauszufinden. Es war das erste, woran ich dachte: P)

Betrachtet man eine DDD, bei der der Klassenhund von caninus und pet ausgeht, hat ein caninus eine Variable, die die Menge an Futter angibt, die er essen soll (eine ganze Zahl), unter dem Namen dietKg, aber ein Pet hat für diesen Zweck gewöhnlich auch eine andere Variable unter derselben name (es sei denn, Sie setzen einen anderen Variablennamen, dann müssen Sie zusätzlichen Code codieren, was das ursprüngliche Problem war, das Sie vermeiden wollten, um die Integrität der Variablen zu behandeln und beizubehalten). Dann haben Sie genau zwei Speicherplätze Um dies zu vermeiden, müssen Sie Ihren Compiler ändern, um diesen Namen unter demselben Namespace zu erkennen, und diesen Daten nur einen einzigen Speicherbereich zuweisen, der leider in der Kompilierungszeit nicht eindeutig festgelegt 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, auf welchen Speicherplatz 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, ob Ihre Art derjenige ist, der die Paradigmen ändert;), und ich sage nicht, dass dies unmöglich ist (aber viele Annahmen und Ein mehrphasiger Compiler muss 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 dazu in der Lage ist (Mehrfachvererbung), lassen Sie es mich bitte wissen Ich freue mich, mich Ihrem Team anzuschließen.

Ordiel
quelle
1
Ab wann könnte ein Compiler davon ausgehen, dass Variablen mit demselben Namen aus verschiedenen übergeordneten Klassen sicher kombiniert werden können? Welche Garantie haben Sie, dass caninus.diet und pet.diet tatsächlich den gleichen Zweck erfüllen? Was würden Sie mit gleichnamigen Funktionen / Methoden tun?
Mr.Mindor
hahaha ich stimme dir vollkommen zu @ Mr.Mindor das ist genau das, was ich in meiner Antwort sage, der einzige Weg, der mir in den Sinn kommt, um an diesen Ansatz heranzukommen, ist prototypenorientiertes Programmieren, aber ich sage nicht, dass es unmöglich ist , und das ist genau der Grund, warum ich sagte, dass viele Annahmen während der Kompilierungszeit gemacht werden müssen und einige zusätzliche "Daten" / Spezifikationen vom Programmierer geschrieben werden müssen (dennoch ist es mehr Codierung, was das ursprüngliche Problem war)
Ordiel
-1

Mir ist schon lange nicht mehr so ​​recht aufgefallen, wie sehr sich einige Programmieraufgaben von anderen unterscheiden - und wie hilfreich es ist, wenn die verwendeten Sprachen und Muster auf den Problemraum zugeschnitten sind.

Wenn Sie alleine oder größtenteils isoliert an Code arbeiten, den Sie geschrieben haben, ist dies ein völlig anderer Problembereich als das Erben einer Codebasis von 40 Personen in Indien, die ein Jahr lang daran gearbeitet haben, bevor sie es Ihnen ohne Übergangshilfe ausgehändigt haben.

Stellen Sie sich vor, Sie wurden gerade von Ihrer Traumfirma eingestellt und haben dann eine solche Codebasis geerbt. Stellen Sie sich außerdem vor, die Berater hätten etwas über Vererbung und Mehrfachvererbung gelernt (und waren daher von dieser fasziniert) ... Stellen Sie sich vor, woran Sie möglicherweise arbeiten.

Wenn Sie Code erben, ist das wichtigste Merkmal, dass er verständlich ist und die Teile isoliert sind, sodass sie unabhängig voneinander bearbeitet werden können. Sicher, wenn Sie die Code-Strukturen zum ersten Mal schreiben, wie Mehrfachvererbung, ersparen Sie sich möglicherweise ein wenig Doppelarbeit und passen zu Ihrer damaligen logischen Stimmung, aber der nächste Typ hat einfach mehr zu entwirren.

Jede Verknüpfung in Ihrem Code erschwert es auch, Teile unabhängig voneinander zu verstehen und zu ändern, und zwar doppelt, wenn mehrere Elemente vererbt werden.

Wenn Sie als Teil eines Teams arbeiten, möchten Sie auf den einfachsten Code abzielen, der Ihnen absolut keine redundante Logik bietet. (Dies bedeutet DRY, nicht, dass Sie nicht viel eingeben sollten, nur, dass Sie Ihren Code nie in 2 ändern müssen Orte, um ein Problem zu lösen!)

Es gibt einfachere Möglichkeiten, DRY-Code zu erstellen als Multiple Inheritance. Wenn Sie ihn also in eine Sprache aufnehmen, können Sie nur auf Probleme stoßen, die von anderen Personen verursacht wurden, die möglicherweise nicht den gleichen Kenntnisstand wie Sie haben. Es ist nur verlockend, wenn Ihre Sprache nicht in der Lage ist, Ihnen eine einfache / weniger komplexe Möglichkeit zu bieten, Ihren Code trocken zu halten.

Bill K
quelle
Stellen Sie sich außerdem vor, dass die Berater etwas gelernt haben - ich habe so viele Nicht-MI-Sprachen (z. B. .net) gesehen, in denen die Berater mit schnittstellenbasierter DI und IoC verrückt geworden sind, um das Ganze zu einem undurchdringlichen Durcheinander zu machen. MI macht das nicht schlimmer, macht es wahrscheinlich besser.
gbjbaanb
@gbjbaanb Sie haben Recht, es ist leicht für jemanden, der noch nicht in Brand gesteckt wurde, eine Funktion durcheinander zu bringen. Tatsächlich ist es fast eine Voraussetzung, eine Funktion zu erlernen, die Sie durcheinander bringen, um herauszufinden, wie Sie sie am besten nutzen können. Ich bin ein bisschen neugierig, denn Sie scheinen zu sagen, dass es nicht mehr DI / IoC für MI-Forces gibt, was ich nicht gesehen habe - ich würde nicht glauben, dass der Beratercode, den Sie mit all den beschissenen Interfaces / DI erhalten haben, gewesen wäre Es ist besser, einen ganzen verrückten Viele-zu-Viele-Erbschaftsbaum zu haben, aber es könnte meine Unerfahrenheit mit MI sein. Haben Sie eine Seite, die ich zum Ersetzen von DI / IoC durch MI lesen könnte?
Bill K
-3

Das Hauptargument gegen Mehrfachvererbung ist, dass einige nützliche Fähigkeiten bereitgestellt werden können und einige nützliche Axiome in einem Rahmen gelten können, der sie stark einschränkt (*), aber nicht ohne solche Einschränkungen bereitgestellt und / oder gehalten werden kann. Unter ihnen:

  • Die Möglichkeit, mehrere separat kompilierte Module zu haben, umfasst Klassen, die von den Klassen anderer Module erben, und die Neukompilierung eines Moduls, das eine Basisklasse enthält, ohne dass jedes Modul neu kompiliert werden muss, das von dieser Basisklasse erbt.

  • Die Fähigkeit eines Typs, eine Member-Implementierung von einer übergeordneten Klasse zu erben, ohne dass der abgeleitete Typ sie erneut implementieren muss

  • Das Axiom, dass jede Objektinstanz direkt auf sich selbst oder einen ihrer Basistypen aufwärts oder abwärts gerichtet sein kann, und solche Aufwärts- und Abwärtsbewegungen in beliebiger Kombination oder Reihenfolge sind immer identitätserhaltend

  • Das Axiom, dass, wenn eine abgeleitete Klasse ein Basisklassenmitglied überschreibt und verkettet, das Basismitglied direkt durch den Verkettungscode aufgerufen wird und nirgendwo anders aufgerufen wird.

(*) In der Regel müssen Typen, die Mehrfachvererbung unterstützen, als "Interfaces" deklariert werden, anstatt als Klassen, und Interfaces dürfen nicht alles tun, was normale Klassen können.

Wenn man eine verallgemeinerte Mehrfachvererbung zulassen will, muss etwas anderes nachgeben. Wenn X und Y beide von B erben, überschreiben sie dasselbe Element M und verketten es mit der Basisimplementierung. Wenn D von X und Y erbt, aber M nicht überschreibt, dann sollte (( B) q) .M () tun? Das Ablehnen einer solchen Besetzung verstößt gegen das Axiom, wonach jedes Objekt auf jeden Basistyp hochgestuft werden kann, aber jedes mögliche Verhalten der Besetzung und des Aufrufs von Mitgliedern verstößt gegen das Axiom der Methodenverkettung. Es kann erforderlich sein, dass Klassen nur in Kombination mit der jeweiligen Version einer Basisklasse geladen werden, für die sie kompiliert wurden. Dies ist jedoch häufig umständlich. Wenn sich die Laufzeitumgebung weigert, einen beliebigen Typ zu laden, in dem ein beliebiger Vorfahr auf mehr als einer Route erreichbar ist, ist dies möglicherweise möglich. würde aber den Nutzen der Mehrfachvererbung stark einschränken. Das Zulassen gemeinsamer Vererbungspfade nur dann, wenn keine Konflikte vorliegen, würde Situationen schaffen, in denen eine alte Version von X mit einem alten oder neuen Y kompatibel wäre und ein altes Y mit einem alten oder neuen X kompatibel wäre, das neue X und das neue Y jedoch kompatibel sein, auch wenn nichts getan hat, von dem an und für sich eine bahnbrechende Veränderung erwartet werden sollte.

Einige Sprachen und Frameworks erlauben eine Mehrfachvererbung. Die Theorie besagt, dass das, was aus MI gewonnen wird, wichtiger ist als das, was aufgegeben werden muss, um dies zu ermöglichen. Die Kosten für MI sind jedoch erheblich, und in vielen Fällen bieten Schnittstellen 90% der Vorteile von MI zu einem geringen Teil der Kosten.

Superkatze
quelle
Würden Abwähler behaupten, einen Kommentar abzugeben? Ich bin der Meinung, dass meine Antwort Informationen enthält, die in anderen Sprachen nicht vorhanden sind, und sich auf semantische Vorteile beziehen, die durch die Nichtzulassung der Mehrfachvererbung erzielt werden können. Die Tatsache, dass das Zulassen einer Mehrfachvererbung das Aufgeben anderer nützlicher Dinge erfordert, ist ein guter Grund für jeden, der diese anderen Dinge mehr schätzt als die Mehrfachvererbung, sich gegen MI zu stellen.
Superkatze
2
Die Probleme mit MI lassen sich mit den gleichen Techniken, die Sie bei der einmaligen Vererbung anwenden, leicht lösen oder ganz vermeiden. Keine der Fähigkeiten / Axiome, die Sie erwähnt haben, wird von MI inhärent behindert, mit Ausnahme der zweiten. Wenn Sie von zwei Klassen erben, die für dieselbe Methode unterschiedliche Verhaltensweisen aufweisen, müssen Sie diese Mehrdeutigkeit trotzdem auflösen. Wenn Sie dies jedoch tun müssen, missbrauchen Sie wahrscheinlich bereits die Vererbung.
CHAO
@cHao: Wenn man verlangt, dass abgeleitete Klassen übergeordnete Methoden überschreiben und nicht mit ihnen verketten müssen (gleiche Regel wie Schnittstellen), kann man die Probleme vermeiden, aber das ist eine ziemlich strenge Einschränkung. Wenn man ein Muster verwendet, bei dem FirstDerivedFoo"override of" Barnichts anderes tut als "chain to a protected non-virtual" FirstDerivedFoo_Barund SecondDerivedFoo"override chains to protected non-virtual" SecondDerivedBar, und wenn Code, der auf die Basismethode zugreifen möchte, diese geschützten nicht virtuellen Methoden verwendet, dann das könnte ein praktikabler Ansatz sein ...
Supercat
... (die Syntax meiden base.VirtualMethod), aber ich kenne keine Sprachunterstützung, um einen solchen Ansatz besonders zu erleichtern. Ich wünschte, es gäbe eine saubere Möglichkeit, einen Codeblock sowohl an eine nicht virtuelle geschützte Methode als auch an eine virtuelle Methode mit derselben Signatur anzuhängen, ohne dass eine virtuelle Einzeilenmethode erforderlich wäre (die viel mehr als eine Zeile beansprucht) im Quellcode).
Supercat
In MI gibt es keine inhärente Anforderung, dass Klassen keine Basismethoden aufrufen, und es wird kein besonderer Grund dafür benötigt. Es scheint ein bisschen seltsam, MI zu beschuldigen, wenn man bedenkt, dass das Problem auch SI betrifft.
CHAO