Ich habe immer gelesen, dass Kompositionen der Vererbung vorzuziehen sind. Ein Blogbeitrag über andere Arten zum Beispiel befürwortet die Verwendung von Komposition anstelle von Vererbung, aber ich kann nicht sehen, wie Polymorphismus erreicht wird.
Aber ich habe das Gefühl, wenn Leute sagen, sie bevorzugen Komposition, dann meinen sie wirklich, sie bevorzugen eine Kombination aus Komposition und Schnittstellenimplementierung. Wie wirst du Polymorphismus ohne Vererbung bekommen?
Hier ist ein konkretes Beispiel, in dem ich Vererbung verwende. Wie würde dies geändert, um Komposition zu verwenden, und was würde ich gewinnen?
Class Shape
{
string name;
public:
void getName();
virtual void draw()=0;
}
Class Circle: public Shape
{
void draw(/*draw circle*/);
}
interfaces
inheritance
composition
MustafaM
quelle
quelle
Antworten:
Polymorphismus bedeutet nicht unbedingt Vererbung. Vererbung wird häufig als einfaches Mittel zum Implementieren von polymorphem Verhalten verwendet, da es praktisch ist, Objekte mit ähnlichem Verhalten so zu klassifizieren, dass sie eine vollständig gemeinsame Stammstruktur und ein gemeinsames Verhalten aufweisen. Denken Sie an all die Auto- und Hundebeispiele, die Sie im Laufe der Jahre gesehen haben.
Aber was ist mit Objekten, die nicht gleich sind? Das Modellieren eines Autos und eines Planeten wäre sehr unterschiedlich, und dennoch möchten beide möglicherweise das Move () - Verhalten implementieren.
Eigentlich haben Sie Ihre eigene Frage beantwortet, als Sie sagten
"But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation."
. Gemeinsames Verhalten kann über Schnittstellen und eine Verhaltenszusammensetzung bereitgestellt werden.Was das Bessere ist, ist die Antwort etwas subjektiv und hängt davon ab, wie Ihr System funktionieren soll, was sowohl inhaltlich als auch architektonisch sinnvoll ist und wie einfach es zu testen und zu warten ist.
quelle
Komposition zu bevorzugen bedeutet nicht nur Polymorphismus. Obwohl das ein Teil davon ist und Sie Recht haben, ist (zumindest in nominal getippten Sprachen), was die Leute wirklich meinen, "eine Kombination aus Komposition und Schnittstellenimplementierung zu bevorzugen". Die Gründe für die Bevorzugung der Komposition (unter vielen Umständen) sind jedoch tiefgreifend.
Beim Polymorphismus geht es um eine Sache, die sich auf verschiedene Arten verhält. Generika / Vorlagen sind insofern ein "polymorphes" Merkmal, als sie es einem einzelnen Codeteil ermöglichen, sein Verhalten mit Typen zu variieren. Tatsächlich verhält sich diese Art von Polymorphismus am besten und wird allgemein als parametrischer Polymorphismus bezeichnet, da die Variation durch einen Parameter definiert wird.
Viele Sprachen bieten eine Form von Polymorphismus, die als "Überladung" oder Ad-hoc-Polymorphismus bezeichnet wird, wobei mehrere gleichnamige Prozeduren ad hoc definiert werden und eine von der Sprache ausgewählt wird (möglicherweise die spezifischste). Dies ist die am wenigsten benommene Art von Polymorphismus, da nichts das Verhalten der beiden Verfahren verbindet, außer der entwickelten Konvention.
Eine dritte Art von Polymorphismus ist der Subtyp Polymorphismus . Hier kann eine Prozedur, die für einen bestimmten Typ definiert ist, auch für eine ganze Familie von "Untertypen" dieses Typs arbeiten. Wenn Sie eine Schnittstelle implementieren oder eine Klasse erweitern, erklären Sie im Allgemeinen Ihre Absicht, einen Untertyp zu erstellen. Wahre Untertypen unterliegen dem Liskovschen Substitutionsprinzip, die besagt, dass wenn Sie etwas über alle Objekte in einem Supertyp beweisen können, Sie es über alle Instanzen in einem Subtyp beweisen können. Das Leben wird jedoch gefährlich, da Menschen in Sprachen wie C ++ und Java im Allgemeinen nicht erzwungene und oft undokumentierte Annahmen über Klassen haben, die in Bezug auf ihre Unterklassen wahr sein können oder nicht. Das heißt, Code wird so geschrieben, als wäre mehr nachweisbar als er tatsächlich ist, was eine ganze Reihe von Problemen mit sich bringt, wenn Sie unachtsam einen Untertyp eingeben.
Die Vererbung ist eigentlich unabhängig vom Polymorphismus. Wenn ein Ding "T" einen Verweis auf sich selbst hat, geschieht die Vererbung, wenn Sie ein neues Ding "S" aus "T" erstellen und den Verweis von "T" auf sich selbst durch einen Verweis auf "S" ersetzen. Diese Definition ist absichtlich vage, da die Vererbung in vielen Situationen vorkommen kann. Am häufigsten wird jedoch eine Unterklasse für ein Objekt festgelegt, bei der der
this
von virtuellen Funktionen aufgerufene Zeiger durch denthis
Zeiger auf den Untertyp ersetzt wird.Vererbung ist gefährlich, wie alle sehr mächtigen Dinge, bei denen Vererbung die Macht hat, Chaos zu verursachen. Angenommen, Sie überschreiben eine Methode, wenn Sie von einer Klasse erben: Alles ist in Ordnung und gut, bis eine andere Methode dieser Klasse davon ausgeht, dass sich die von Ihnen geerbte Methode auf eine bestimmte Weise verhält . Sie können sich teilweise davor schützen, indem Sie alle von einer anderen Ihrer Methoden aufgerufenen Methoden als privat oder nicht virtuell (final) deklarieren, es sei denn, sie sind so konzipiert , dass sie überschrieben werden. Auch das ist nicht immer gut genug. Manchmal sieht man vielleicht so etwas (in Pseudo-Java, hoffentlich lesbar für C ++ und C # -Benutzer)
Sie finden das schön und haben Ihre eigene Art, "Dinge" zu tun, aber Sie nutzen die Vererbung, um die Fähigkeit zu erlangen, "moreThings" zu tun.
Und alles ist gut und schön.
WayOfDoingUsefulThings
wurde so konzipiert, dass das Ersetzen einer Methode die Semantik einer anderen Methode nicht verändert ... außer warten, nein, war es nicht. Es sieht einfach so aus, als wäre es so, aber derdoThings
veränderbare Zustand hat sich geändert, was wichtig war. Also, obwohl es keine überschreibbaren Funktionen aufgerufen hat,Jetzt macht man etwas anderes als erwartet, wenn man es einem übergibt
MyUsefulThings
. Was noch schlimmer ist, vielleicht wissen Sie gar nicht, dassWayOfDoingUsefulThings
das diese Versprechungen gemacht hat.dealWithStuff
Kommt möglicherweise aus derselben Bibliothek wieWayOfDoingUsefulThings
undgetStuff()
wird nicht einmal von der Bibliothek exportiert (denken Sie an Freundesklassen in C ++). Noch schlimmer ist, haben Sie die statischen Kontrollen der Sprache , ohne es zu merken besiegt:dealWithStuff
nahm ein ,WayOfDoingUsefulThings
nur um sicher zu stellen , dass es eine hättegetStuff()
Funktion , die eine bestimmte Art und Weise verhalten hat .Komposition verwenden
bringt statische Sicherheit zurück. Im Allgemeinen ist Komposition einfacher zu verwenden und sicherer als Vererbung bei der Implementierung von Subtyping. Sie können damit auch final-Methoden überschreiben, was bedeutet, dass Sie die meiste Zeit alles final / non-virtual deklarieren können, mit Ausnahme von Schnittstellen.
In einer besseren Welt würden Sprachen automatisch das Boilerplate mit einem
delegation
Schlüsselwort einfügen . Die meisten tun das nicht. Ein Nachteil sind größere Klassen. Sie können jedoch Ihre IDE veranlassen, die delegierende Instanz für Sie zu schreiben.Jetzt geht es im Leben nicht nur um Polymorphismus. Sie müssen nicht immer einen Untertyp eingeben. Das Ziel des Polymorphismus ist im Allgemeinen die Wiederverwendung von Code, aber es ist nicht der einzige Weg, dieses Ziel zu erreichen. Häufig ist es sinnvoll, die Komposition ohne Subtyp-Polymorphismus zur Verwaltung der Funktionalität zu verwenden.
Auch die Verhaltensvererbung hat ihre Verwendung. Es ist eine der mächtigsten Ideen in der Informatik. Es ist nur so, dass in den meisten Fällen gute OOP-Anwendungen nur mithilfe von Schnittstellenvererbung und Kompositionen geschrieben werden können. Die zwei Prinzipien
sind aus den oben genannten Gründen ein guter Leitfaden und verursachen keine erheblichen Kosten.
quelle
Der Grund, warum die Leute dies sagen, ist, dass OOP-Programmierer, die ihre Vorlesungen über Polymorphismus durch Vererbung noch nicht abgeschlossen haben, dazu neigen, große Klassen mit vielen polymorphen Methoden zu schreiben.
Ein typisches Beispiel stammt aus der Welt der Spieleentwicklung. Angenommen, Sie haben eine Basisklasse für alle Ihre Spieleinheiten - das Raumschiff des Spielers, Monster, Kugeln usw .; Jeder Entitätstyp hat eine eigene Unterklasse. Der Vererbungs Ansatz würde einige polymorphen Methoden verwenden, zum Beispiel
update_controls()
,update_physics()
,draw()
usw., und setzt sie für jede Unterklasse. Dies bedeutet jedoch, dass Sie nicht verwandte Funktionen koppeln: Es ist unerheblich, wie ein Objekt aussieht, um es zu bewegen, und Sie müssen nichts über seine KI wissen, um es zu zeichnen. Der Kompositionsansatz definiert stattdessen mehrere Basisklassen (oder Interfaces), z. B.EntityBrain
(Unterklassen implementieren KI- oder Spielereingaben),EntityPhysics
(Unterklassen implementieren Bewegungsphysik) undEntityPainter
(Unterklassen kümmern sich um das Zeichnen) sowie eine nicht polymorphe KlasseEntity
das hält eine Instanz von jedem. Auf diese Weise können Sie jedes Erscheinungsbild mit jedem Physikmodell und jeder KI kombinieren, und da Sie sie getrennt halten, wird auch Ihr Code viel sauberer. Auch Probleme wie "Ich möchte ein Monster, das wie das Ballon-Monster in Level 1 aussieht, sich aber wie der verrückte Clown in Level 15 verhält", verschwinden: Nehmen Sie einfach die passenden Komponenten und kleben Sie sie zusammen.Beachten Sie, dass der Kompositionsansatz weiterhin die Vererbung in jeder Komponente verwendet. obwohl Sie hier im Idealfall nur Schnittstellen und ihre Implementierungen verwenden würden.
"Trennung von Anliegen" ist hier der Schlüsselbegriff: Die Darstellung der Physik, die Implementierung einer KI und das Zeichnen einer Entität sind drei Anliegen, deren Zusammenfassung zu einer Entität ein viertes ist. Beim kompositorischen Ansatz wird jedes Unternehmen als eine Klasse modelliert.
quelle
MonsterVisuals balloonVisuals
undMonsterBehaviour crazyClownBehaviour
und instanziieren aMonster(balloonVisuals, crazyClownBehaviour)
, zusammen mit den InstanzenMonster(balloonVisuals, balloonBehaviour)
undMonster(crazyClownVisuals, crazyClownBehaviour)
, die auf Stufe 1 und Stufe 15 instanziiert wurdenDas Beispiel, das Sie angegeben haben, ist eines, bei dem Vererbung die natürliche Wahl ist. Ich glaube nicht, dass irgendjemand behaupten würde, dass Komposition immer eine bessere Wahl ist als Vererbung - es ist nur eine Richtlinie, die bedeutet, dass es oft besser ist, mehrere relativ einfache Objekte zusammenzusetzen, als viele hochspezialisierte Objekte zu erstellen.
Die Delegierung ist ein Beispiel für die Verwendung der Komposition anstelle der Vererbung. Mit der Delegierung können Sie das Verhalten einer Klasse ohne Unterklassen ändern. Stellen Sie sich die Klasse NetStream vor, die eine Netzwerkverbindung bereitstellt. Es kann natürlich sein, NetStream als Unterklasse einzustufen, um ein gemeinsames Netzwerkprotokoll zu implementieren, sodass Sie möglicherweise FTPStream und HTTPStream entwickeln. Anstatt jedoch eine sehr spezifische HTTPStream-Unterklasse für einen einzigen Zweck zu erstellen, z. B. UpdateMyWebServiceHTTPStream, ist es oft besser, eine einfache alte Instanz von HTTPStream zusammen mit einem Delegaten zu verwenden, der weiß, wie er mit den von diesem Objekt empfangenen Daten umzugehen hat. Ein Grund dafür ist, dass die Anzahl der Klassen, die beibehalten werden müssen, die Sie jedoch nie wieder verwenden können, nicht zunimmt.
quelle
Sie werden diesen Zyklus im Softwareentwicklungsdiskurs viel sehen:
Einige Funktionen oder Muster (nennen wir sie "Muster X") haben sich für einen bestimmten Zweck als nützlich erwiesen. In Blog-Posts werden die Tugenden von Pattern X gepriesen.
Der Hype führt einige Leute zu denken , sollten Sie Muster X verwenden , wann immer möglich .
Andere Leute ärgern sich darüber, dass Pattern X in Kontexten verwendet wird, in denen es nicht angemessen ist, und sie schreiben Blog-Posts, in denen sie erklären, dass Sie Pattern X nicht immer verwenden sollten und dass es in bestimmten Kontexten schädlich ist.
Das Spiel lässt manche Leute glauben, dass Muster X immer schädlich ist und niemals verwendet werden sollte.
Sie werden feststellen, dass dieser Hype / Backlash-Zyklus bei fast allen Funktionen auftritt, von
GOTO
Mustern über SQL bis hin zu NoSQL und Vererbung. Das Gegenmittel ist, immer den Kontext zu berücksichtigen .Nachdem
Circle
Abstieg ausShape
ist genau wie die Vererbung soll in OO - Sprachen verwendet werden , unterstützen Vererbung.Die Faustregel "Komposition vor Vererbung" ist ohne Kontext wirklich irreführend. Sie sollten die Vererbung vorziehen, wenn die Vererbung angemessener ist, aber die Komposition vorziehen, wenn die Komposition angemessener ist. Der Satz richtet sich an Personen im Stadium 2 des Hype-Zyklus, die der Meinung sind, dass Vererbung überall eingesetzt werden sollte. Aber der Kreislauf hat sich weiterentwickelt, und heute scheinen einige Leute zu glauben, die Vererbung sei an sich schon schlecht.
Stellen Sie es sich vor wie einen Hammer gegen einen Schraubenzieher. Sollten Sie einen Schraubenzieher einem Hammer vorziehen? Die Frage ergibt keinen Sinn. Sie sollten das für den Job geeignete Tool verwenden, und alles hängt davon ab, welche Aufgabe Sie erledigen müssen.
quelle