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 den this
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)
interface UsefulThingsInterface {
void doThings();
void doMoreThings();
}
...
class WayOfDoingUsefulThings implements UsefulThingsInterface{
private foo stuff;
public final int getStuff();
void doThings(){
//modifies stuff, such that ...
...
}
...
void doMoreThings(){
//ignores stuff
...
}
}
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.
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
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 der doThings
veränderbare Zustand hat sich geändert, was wichtig war. Also, obwohl es keine überschreibbaren Funktionen aufgerufen hat,
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
Jetzt macht man etwas anderes als erwartet, wenn man es einem übergibt MyUsefulThings
. Was noch schlimmer ist, vielleicht wissen Sie gar nicht, dass WayOfDoingUsefulThings
das diese Versprechungen gemacht hat. dealWithStuff
Kommt möglicherweise aus derselben Bibliothek wie WayOfDoingUsefulThings
und getStuff()
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ätte getStuff()
Funktion , die eine bestimmte Art und Weise verhalten hat .
Komposition verwenden
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
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
- Vererbung verbieten oder Design dafür
- Ich bevorzuge die Komposition
sind aus den oben genannten Gründen ein guter Leitfaden und verursachen keine erheblichen Kosten.