Antworten:
Mehrfachvererbung (abgekürzt als MI) riecht , was bedeutet, dass dies normalerweise aus schlechten Gründen durchgeführt wurde und dem Betreuer ins Gesicht bläst.
Dies gilt für die Vererbung und umso mehr für die Mehrfachvererbung.
Muss Ihr Objekt wirklich von einem anderen erben? A Car
muss nicht von a erben, um Engine
zu arbeiten, noch von a Wheel
. A Car
hat eine Engine
und vier Wheel
.
Wenn Sie die Mehrfachvererbung verwenden, um diese Probleme anstelle der Komposition zu lösen, haben Sie etwas falsch gemacht.
Normalerweise müssen Sie eine Klasse A
, dann B
und C
beide vererben A
. Und (frag mich nicht warum) jemand entscheidet dann, dass er D
sowohl von B
als auch erben muss C
.
Ich bin in 8 acht Jahren zweimal auf diese Art von Problem gestoßen, und es ist amüsant zu sehen, weil:
D
sollte nicht von beiden geerbt werden B
und C
), weil dies eine schlechte Architektur war (in der Tat C
sollte überhaupt nicht existieren ...)A
in ihrer Enkelklasse zweimal vorhanden D
war. Daher A::field
bedeutete das Aktualisieren eines übergeordneten Felds, dass es entweder zweimal (durch B::field
und C::field
) aktualisiert wurde oder dass später etwas lautlos schief ging und abstürzte (Zeiger neu einfügen B::field
und löschen C::field
...)Wenn Sie das Schlüsselwort virtual in C ++ verwenden, um die Vererbung zu qualifizieren, wird das oben beschriebene doppelte Layout vermieden, wenn dies nicht das ist, was Sie wollen, aber meiner Erfahrung nach machen Sie wahrscheinlich etwas falsch ...
In der Objekthierarchie sollten Sie versuchen, die Hierarchie als Baum (ein Knoten hat EIN übergeordnetes Element) und nicht als Diagramm beizubehalten.
Das eigentliche Problem mit dem Diamond of Dread in C ++ ( vorausgesetzt, das Design ist solide - lassen Sie Ihren Code überprüfen! ) Ist , dass Sie eine Auswahl treffen müssen :
A
zweimal in Ihrem Layout vorhanden ist, und was bedeutet das? Wenn ja, dann erben Sie auf jeden Fall zweimal davon.Diese Wahl ist dem Problem inhärent, und in C ++ können Sie dies im Gegensatz zu anderen Sprachen tatsächlich tun, ohne dass ein Dogma Ihr Design auf Sprachebene erzwingt.
Aber wie bei allen Kräften geht auch bei dieser Macht die Verantwortung einher: Lassen Sie Ihr Design überprüfen.
Die Mehrfachvererbung von null oder einer konkreten Klasse und null oder mehr Schnittstellen ist normalerweise in Ordnung, da Sie den oben beschriebenen Diamond of Dread nicht antreffen. In der Tat wird dies in Java so gemacht.
Normalerweise meinen Sie, wenn C von A
und erbt, B
dass Benutzer verwenden können, C
als ob es ein A
und / oder als ob es ein wäre B
.
In C ++ ist eine Schnittstelle eine abstrakte Klasse mit:
Die Mehrfachvererbung von null zu einem realen Objekt und null oder mehr Schnittstellen wird nicht als "stinkend" angesehen (zumindest nicht so viel).
Erstens kann das NVI-Muster verwendet werden, um eine Schnittstelle zu erzeugen, da das eigentliche Kriterium darin besteht, keinen Status zu haben (dh keine Mitgliedsvariablen, außer this
). Der Sinn Ihrer abstrakten Schnittstelle besteht darin, einen Vertrag zu veröffentlichen ("Sie können mich so und so nennen"), nicht mehr und nicht weniger. Die Einschränkung, nur eine abstrakte virtuelle Methode zu haben, sollte eine Entwurfsentscheidung sein, keine Verpflichtung.
Zweitens ist es in C ++ sinnvoll, virtuell von abstrakten Schnittstellen zu erben (auch mit den zusätzlichen Kosten / Indirektion). Wenn Sie dies nicht tun und die Schnittstellenvererbung in Ihrer Hierarchie mehrmals angezeigt wird, treten Unklarheiten auf.
Drittens ist die Objektorientierung großartig, aber es ist nicht die einzige Wahrheit da draußen TM in C ++. Verwenden Sie die richtigen Tools und denken Sie immer daran, dass Sie in C ++ andere Paradigmen haben, die andere Arten von Lösungen anbieten.
Manchmal ja.
Normalerweise C
erbt Ihre Klasse von A
und B
und A
und B
sind zwei nicht verwandte Objekte (dh nicht in derselben Hierarchie, nichts gemeinsam, unterschiedliche Konzepte usw.).
Sie könnten beispielsweise ein System Nodes
mit X-, Y- und Z-Koordinaten haben, das viele geometrische Berechnungen durchführen kann (möglicherweise einen Punkt, einen Teil geometrischer Objekte), und jeder Knoten ist ein automatisierter Agent, der mit anderen Agenten kommunizieren kann.
Vielleicht haben Sie bereits Zugriff auf zwei Bibliotheken mit jeweils einem eigenen Namespace (ein weiterer Grund für die Verwendung von Namespaces ... Aber Sie verwenden Namespaces, nicht wahr?), Ein Wesen geo
und das andere Wesenai
Sie haben also Ihre eigenen own::Node
Ableitungen von ai::Agent
und geo::Point
.
Dies ist der Moment, in dem Sie sich fragen sollten, ob Sie stattdessen keine Komposition verwenden sollten. Wenn own::Node
es wirklich sowohl a ai::Agent
als auch a ist geo::Point
, reicht die Komposition nicht aus.
Dann benötigen Sie mehrere Vererbungen, damit Sie own::Node
mit anderen Agenten entsprechend ihrer Position in einem 3D-Raum kommunizieren können.
(Sie werden feststellen, dass ai::Agent
und geo::Point
vollständig, vollständig, vollständig unabhängig sind ... Dies verringert die Gefahr einer Mehrfachvererbung drastisch.)
Es gibt andere Fälle:
this
).Manchmal kann man Komposition verwenden, und manchmal ist MI besser. Der Punkt ist: Sie haben die Wahl. Tun Sie es verantwortungsbewusst (und lassen Sie Ihren Code überprüfen).
Nach meiner Erfahrung die meiste Zeit nein. MI ist nicht das richtige Werkzeug, auch wenn es zu funktionieren scheint, da es von den Faulen verwendet werden kann, um Features zu stapeln, ohne die Konsequenzen zu erkennen (wie das Erstellen eines Car
sowohl als auch Engine
eines Wheel
).
Aber manchmal ja. Und zu diesem Zeitpunkt wird nichts besser funktionieren als MI.
Aber weil MI stinkt, sollten Sie darauf vorbereitet sein, Ihre Architektur in Codeüberprüfungen zu verteidigen (und es zu verteidigen ist eine gute Sache, denn wenn Sie es nicht verteidigen können, sollten Sie es nicht tun).
Aus einem Interview mit Bjarne Stroustrup :
Die Leute sagen ganz richtig, dass Sie keine Mehrfachvererbung benötigen, denn alles, was Sie mit Mehrfachvererbung tun können, können Sie auch mit Einzelvererbung tun. Sie verwenden nur den von mir erwähnten Delegationstrick. Darüber hinaus benötigen Sie überhaupt keine Vererbung, da alles, was Sie mit Einzelvererbung tun, auch ohne Vererbung durch Weiterleiten durch eine Klasse erfolgen kann. Eigentlich brauchen Sie auch keine Klassen, weil Sie alles mit Zeigern und Datenstrukturen machen können. Aber warum willst du das tun? Wann ist es bequem, die Sprachfunktionen zu nutzen? Wann würden Sie eine Problemumgehung bevorzugen? Ich habe Fälle gesehen, in denen Mehrfachvererbung nützlich ist, und ich habe sogar Fälle gesehen, in denen ziemlich komplizierte Mehrfachvererbung nützlich ist. Im Allgemeinen bevorzuge ich die von der Sprache angebotenen Funktionen, um Problemumgehungen durchzuführen
const
- habe ich musste klobig Abhilfen schreiben ( in der Regel Schnittstellen und Zusammensetzung verwendet wird ) , wenn eine Klasse wirklich benötigt mutable- und unveränderlich-Varianten zu haben. Ich habe jedoch noch nie eine Mehrfachvererbung verpasst und hatte nie das Gefühl, dass ich aufgrund des Fehlens dieser Funktion eine Problemumgehung schreiben musste. Das ist der Unterschied. In jedem Fall habe ich je gesehen nicht mit MI ist die bessere Wahl Design, keine Abhilfe.
Es gibt keinen Grund, dies zu vermeiden, und es kann in Situationen sehr nützlich sein. Sie müssen sich jedoch der potenziellen Probleme bewusst sein.
Der größte ist der Diamant des Todes:
class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;
Sie haben jetzt zwei "Kopien" von GrandParent in Child.
C ++ hat jedoch darüber nachgedacht und ermöglicht Ihnen die virtuelle Vererbung, um die Probleme zu umgehen.
class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;
Überprüfen Sie immer Ihr Design und stellen Sie sicher, dass Sie keine Vererbung verwenden, um bei der Wiederverwendung von Daten zu sparen. Wenn Sie dasselbe mit Komposition darstellen können (und normalerweise auch), ist dies ein weitaus besserer Ansatz.
GrandParent
in Child
. Es besteht die Angst vor MI, weil die Leute einfach denken, sie könnten die Sprachregeln nicht verstehen. Aber wer diese einfachen Regeln nicht bekommen kann, kann auch kein nicht triviales Programm schreiben.
Siehe w: Mehrfachvererbung .
Mehrfachvererbung wurde kritisiert und ist daher in vielen Sprachen nicht implementiert. Kritik umfasst:
- Erhöhte Komplexität
- Semantische Ambiguität wird oft als Diamantproblem zusammengefasst .
- Es ist nicht möglich, mehrere Male explizit von einer einzelnen Klasse zu erben
- Reihenfolge der Vererbung Änderung der Klassensemantik.
Die Mehrfachvererbung in Sprachen mit Konstruktoren im C ++ / Java-Stil verschärft das Vererbungsproblem von Konstruktoren und der Verkettung von Konstruktoren, wodurch Wartungs- und Erweiterungsprobleme in diesen Sprachen entstehen. Objekte in Vererbungsbeziehungen mit sehr unterschiedlichen Konstruktionsmethoden sind unter dem Konstruktorkettenparadigma schwer zu implementieren.
Moderne Methode zur Lösung dieses Problems, um eine Schnittstelle (reine abstrakte Klasse) wie die COM- und Java-Schnittstelle zu verwenden.
Ich kann stattdessen andere Dinge tun?
Ja, du kannst. Ich werde von GoF stehlen .
Öffentliche Vererbung ist eine IS-A-Beziehung, und manchmal ist eine Klasse eine Art von mehreren verschiedenen Klassen, und manchmal ist es wichtig, dies zu reflektieren.
"Mixins" sind manchmal auch nützlich. Es handelt sich im Allgemeinen um kleine Klassen, die normalerweise nichts erben und nützliche Funktionen bieten.
Solange die Vererbungshierarchie ziemlich flach ist (wie es fast immer sein sollte) und gut verwaltet wird, ist es unwahrscheinlich, dass Sie die gefürchtete Diamantvererbung erhalten. Der Diamant ist nicht bei allen Sprachen ein Problem, die Mehrfachvererbung verwenden, aber die Behandlung von C ++ ist häufig umständlich und manchmal rätselhaft.
Ich bin zwar auf Fälle gestoßen, in denen Mehrfachvererbung sehr praktisch ist, aber sie sind tatsächlich ziemlich selten. Dies liegt wahrscheinlich daran, dass ich andere Entwurfsmethoden bevorzuge, wenn ich nicht wirklich Mehrfachvererbung benötige. Ich ziehe es vor, verwirrende Sprachkonstrukte zu vermeiden, und es ist einfach, Vererbungsfälle zu konstruieren, in denen Sie das Handbuch wirklich gut lesen müssen, um herauszufinden, was los ist.
Sie sollten Mehrfachvererbung nicht "vermeiden", aber Sie sollten sich der Probleme bewusst sein, die auftreten können, wie z. B. das "Diamantproblem" ( http://en.wikipedia.org/wiki/Diamond_problem ), und die Ihnen übertragene Kraft mit Sorgfalt behandeln , wie du es mit allen Kräften tun solltest.
Auf die Gefahr hin, ein bisschen abstrakt zu werden, finde ich es aufschlussreich, über Vererbung im Rahmen der Kategorietheorie nachzudenken.
Wenn wir an all unsere Klassen und Pfeile zwischen ihnen denken, die Vererbungsbeziehungen bezeichnen, dann so etwas
A --> B
bedeutet, dass class B
abgeleitet von class A
. Beachten Sie, dass gegeben
A --> B, B --> C
wir sagen, C leitet sich von B ab, das von A abgeleitet ist, also soll C auch von A abgeleitet sein, also
A --> C
Darüber hinaus sagen wir, dass unser Vererbungsmodell für jede Klasse A
, von der es trivial A
abgeleitet ist A
, die Definition einer Kategorie erfüllt. In der traditionelleren Sprache haben wir eine Kategorie Class
mit Objekten aller Klassen und Morphismen der Vererbungsbeziehungen.
Das ist ein bisschen Setup, aber damit werfen wir einen Blick auf unseren Diamond of Doom:
C --> D
^ ^
| |
A --> B
Es ist ein schattig aussehendes Diagramm, aber es wird reichen. So D
erbt von allen A
, B
und C
. Und wenn man sich der Frage von OP nähert, D
erbt man auch von jeder Oberklasse von A
. Wir können ein Diagramm zeichnen
C --> D --> R
^ ^
| |
A --> B
^
|
Q
Nun, Probleme, die hier mit dem Diamanten des Todes verbunden sind, sind, wann C
und B
teilen einige Eigenschafts- / Methodennamen und Dinge mehrdeutig werden; Wenn wir jedoch ein gemeinsames Verhalten verschieben, A
verschwindet die Mehrdeutigkeit.
Setzen Sie in kategorisch, wir wollen A
, B
und C
so zu sein , dass , wenn B
und C
vererben Q
dann A
wie als Unterklasse überschrieben werden können Q
. Dies macht so A
etwas wie einen Pushout .
Es gibt auch eine symmetrische Konstruktion, D
die als Pullback bezeichnet wird . Dies ist im Wesentlichen die allgemeinste nützliche Klasse, die Sie erstellen können und die sowohl von B
als auch von erbt C
. Das heißt, wenn Sie eine andere Klasse haben, die R
mehrfach von B
und erbt C
, dann D
ist dies eine Klasse, in R
die als Unterklasse von umgeschrieben werden kann D
.
Wenn Sie sicherstellen, dass Ihre Spitzen des Diamanten Pullbacks und Pushouts sind, können wir Probleme mit Namenskonflikten oder Wartungsarbeiten, die andernfalls auftreten könnten, generisch behandeln.
Hinweis Paercebal ‚s Antwort inspiriert dies als seine Ermahnungen durch das obige Modell gegeben impliziert werden , dass wir in der vollen Kategorie arbeiten Klasse aller möglichen Klassen.
Ich wollte sein Argument auf etwas verallgemeinern, das zeigt, wie kompliziert Mehrfachvererbungsbeziehungen sowohl mächtig als auch unproblematisch sein können.
TL; DR Stellen Sie sich die Vererbungsbeziehungen in Ihrem Programm als eine Kategorie vor. Dann können Sie Diamond of Doom-Probleme vermeiden, indem Sie mehrfach vererbte Klassen-Pushouts und symmetrisch erstellen und eine gemeinsame übergeordnete Klasse erstellen, die ein Pullback ist.
Wir benutzen Eiffel. Wir haben einen ausgezeichneten MI. Keine Sorge. Keine Probleme. Leicht zu handhaben. Es gibt Zeiten, in denen MI NICHT verwendet wird. Es ist jedoch nützlicher, als die Leute glauben, weil sie: A) in einer gefährlichen Sprache sind, die es nicht gut beherrscht -OR- B) zufrieden damit sind, wie sie jahrelang mit MI gearbeitet haben -OR- C) aus anderen Gründen ( zu zahlreich, um sie aufzulisten Ich bin mir ziemlich sicher - siehe Antworten oben).
Für uns ist MI mit Eiffel so natürlich wie alles andere und ein weiteres gutes Werkzeug in der Toolbox. Ehrlich gesagt sind wir nicht besorgt darüber, dass sonst niemand Eiffel benutzt. Keine Sorge. Wir sind zufrieden mit dem, was wir haben und laden Sie ein, einen Blick darauf zu werfen.
Während Sie suchen: Beachten Sie besonders die Leeren-Sicherheit und die Beseitigung der Null-Zeiger-Dereferenzierung. Während wir alle um MI herum tanzen, gehen Ihre Zeiger verloren! :-)
Jede Programmiersprache hat eine etwas andere Behandlung der objektorientierten Programmierung mit Vor- und Nachteilen. Die Version von C ++ legt den Schwerpunkt auf die Leistung und hat den damit verbundenen Nachteil, dass es störend einfach ist, ungültigen Code zu schreiben - und dies gilt für die Mehrfachvererbung. Infolgedessen besteht die Tendenz, Programmierer von dieser Funktion wegzulenken.
Andere Leute haben sich mit der Frage befasst, wozu Mehrfachvererbung nicht gut ist. Aber wir haben einige Kommentare gesehen, die mehr oder weniger implizieren, dass der Grund, dies zu vermeiden, darin besteht, dass es nicht sicher ist. Ja und nein.
Wie es in C ++ häufig der Fall ist, können Sie eine grundlegende Richtlinie sicher verwenden, ohne ständig über die Schulter schauen zu müssen. Die Schlüsselidee ist, dass Sie eine spezielle Art von Klassendefinition unterscheiden, die als "Mix-In" bezeichnet wird. Klasse ist ein Mix-In, wenn alle ihre Mitgliedsfunktionen virtuell (oder rein virtuell) sind. Dann dürfen Sie von einer einzelnen Hauptklasse und so vielen "Mix-Ins" erben, wie Sie möchten - aber Sie sollten Mixins mit dem Schlüsselwort "virtual" erben. z.B
class CounterMixin {
int count;
public:
CounterMixin() : count( 0 ) {}
virtual ~CounterMixin() {}
virtual void increment() { count += 1; }
virtual int getCount() { return count; }
};
class Foo : public Bar, virtual public CounterMixin { ..... };
Mein Vorschlag ist, dass Sie, wenn Sie beabsichtigen, eine Klasse als Mix-In-Klasse zu verwenden, auch eine Namenskonvention anwenden, um es jedem, der den Code liest, zu erleichtern, zu sehen, was passiert, und um zu überprüfen, ob Sie die Regeln der grundlegenden Richtlinie einhalten . Und Sie werden feststellen, dass es viel besser funktioniert, wenn Ihre Mix-Ins auch Standardkonstruktoren haben, nur weil virtuelle Basisklassen funktionieren. Und denken Sie daran, alle Destruktoren auch virtuell zu machen.
Beachten Sie, dass meine Verwendung des Wortes "Mix-In" hier nicht mit der parametrisierten Vorlagenklasse identisch ist ( eine gute Erklärung finden Sie unter diesem Link ), aber ich denke, dass dies eine faire Verwendung der Terminologie ist.
Jetzt möchte ich nicht den Eindruck erwecken, dass dies die einzige Möglichkeit ist, Mehrfachvererbung sicher zu verwenden. Es ist nur eine Möglichkeit, die ziemlich einfach zu überprüfen ist.
Sie sollten es vorsichtig verwenden, es gibt einige Fälle, wie das Diamantproblem , in denen die Dinge kompliziert werden können.
(Quelle: learncpp.com )
Printer
sollte a nicht einmal a sein PoweredDevice
. A Printer
dient zum Drucken und nicht zur Energieverwaltung. Die Implementierung eines bestimmten Druckers muss möglicherweise eine Energieverwaltung durchführen, diese Energieverwaltungsbefehle sollten jedoch nicht direkt den Benutzern des Druckers zugänglich gemacht werden. Ich kann mir keine reale Verwendung dieser Hierarchie vorstellen.
Verwendungen und Missbrauch der Vererbung.
Der Artikel erklärt die Vererbung und ihre Gefahren hervorragend.
Über das Rautenmuster hinaus erschwert die Mehrfachvererbung das Verständnis des Objektmodells, was wiederum die Wartungskosten erhöht.
Komposition ist an sich leicht zu verstehen, zu verstehen und zu erklären. Das Schreiben von Code kann mühsam werden, aber eine gute IDE (es ist ein paar Jahre her, seit ich mit Visual Studio gearbeitet habe, aber sicherlich haben alle Java-IDEs großartige Tools zur Automatisierung von Kompositionsverknüpfungen) sollte Sie über diese Hürde bringen.
Auch in Bezug auf die Wartung tritt das "Diamantproblem" auch in nicht wörtlichen Vererbungsinstanzen auf. Wenn Sie zum Beispiel A und B haben und Ihre Klasse C beide erweitert, und A eine 'makeJuice'-Methode hat, mit der Orangensaft hergestellt wird, und Sie diese Methode erweitern, um Orangensaft mit einem Hauch Limette herzustellen: Was passiert, wenn der Designer für' B 'fügt eine' makeJuice'-Methode hinzu, die elektrischen Strom erzeugt? ‚A‘ und ‚B‘ kann kompatibel „Eltern“ sein gerade jetzt , aber dass sie sein wird , bedeutet nicht immer so!
Insgesamt ist die Maxime, eine Vererbung und insbesondere eine Mehrfachvererbung zu vermeiden, stichhaltig. Wie bei allen Maximen gibt es Ausnahmen, aber Sie müssen sicherstellen, dass ein blinkendes grünes Neonschild auf alle von Ihnen codierten Ausnahmen zeigt (und Ihr Gehirn so trainieren, dass Sie jedes Mal, wenn Sie solche Erbbäume sehen, in Ihrem eigenen blinkenden grünen Neon zeichnen Zeichen), und dass Sie von Zeit zu Zeit überprüfen, ob alles Sinn macht.
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
Uhhh, Sie erhalten natürlich einen Kompilierungsfehler (wenn die Verwendung nicht eindeutig ist).
Das Hauptproblem bei MI von konkreten Objekten ist, dass Sie selten ein Objekt haben, das zu Recht "ein A sein und ein B sein sollte", so dass es aus logischen Gründen selten die richtige Lösung ist. Viel häufiger haben Sie ein Objekt C, das "C kann als A oder B fungieren" befolgt, was Sie über die Vererbung und Zusammensetzung der Schnittstelle erreichen können. Aber machen Sie keinen Fehler - die Vererbung mehrerer Schnittstellen ist immer noch MI, nur eine Teilmenge davon.
Insbesondere für C ++ ist die Hauptschwäche des Features nicht die tatsächliche EXISTENCE of Multiple Inheritance, sondern einige Konstrukte, die es zulässt und die fast immer fehlerhaft sind. Zum Beispiel das Erben mehrerer Kopien desselben Objekts wie:
class B : public A, public A {};
ist durch Definition missgebildet. Übersetzt ins Englische ist dies "B ist ein A und ein A". Selbst in der menschlichen Sprache gibt es also eine starke Mehrdeutigkeit. Meinten Sie "B hat 2 As" oder nur "B ist ein A"?. Das Zulassen eines solchen pathologischen Codes und noch schlimmer ein Anwendungsbeispiel hat C ++ keinen Gefallen getan, wenn es darum ging, die Funktion in Nachfolgesprachen beizubehalten.
Sie können die Komposition der Vererbung vorziehen.
Das allgemeine Gefühl ist, dass die Komposition besser ist und sehr gut diskutiert wird.
The general feeling is that composition is better, and it's very well discussed.
Das heißt nicht, dass die Komposition besser ist.
Es dauert 4/8 Bytes pro betroffener Klasse. (Einer dieser Zeiger pro Klasse).
Dies ist vielleicht nie ein Problem, aber wenn Sie eines Tages eine Mikrodatenstruktur haben, die milliardenfach instanziiert ist, wird dies der Fall sein.