Von: http://www.artima.com/lejava/articles/designprinciples4.html
Erich Gamma: Ich denke, das stimmt auch nach zehn Jahren noch. Vererbung ist eine coole Möglichkeit, das Verhalten zu ändern. Aber wir wissen, dass es spröde ist, weil die Unterklasse leicht Annahmen über den Kontext treffen kann, in dem eine Methode aufgerufen wird, die sie überschreibt. Aufgrund des impliziten Kontexts, in dem der von mir eingesteckte Unterklassencode aufgerufen wird, besteht eine enge Kopplung zwischen der Basisklasse und der Unterklasse. Zusammensetzung hat eine schönere Eigenschaft. Die Kopplung wird reduziert, indem Sie nur einige kleinere Dinge in etwas Größeres stecken, und das größere Objekt ruft das kleinere Objekt zurück. Aus API-Sicht ist die Definition, dass eine Methode überschrieben werden kann, eine stärkere Verpflichtung als die Definition, dass eine Methode aufgerufen werden kann.
Ich verstehe nicht, was er meint. Könnte es bitte jemand erklären?
Wenn Sie eine normale Funktion veröffentlichen, geben Sie einen einseitigen Vertrag ab:
Was macht die Funktion, wenn sie aufgerufen wird?
Wenn Sie einen Rückruf veröffentlichen, geben Sie auch einen einseitigen Vertrag ab:
Wann und wie wird er aufgerufen?
Und wenn Sie eine überschreibbare Funktion veröffentlichen, ist dies beides gleichzeitig, sodass Sie einen beidseitigen Vertrag abschließen:
Wann wird sie aufgerufen, und was muss sie tun, wenn sie aufgerufen wird?
Selbst wenn Ihre Benutzer Ihre API nicht missbrauchen (indem sie ihren Teil des Vertrags brechen , dessen Aufdeckung möglicherweise unerschwinglich ist), können Sie leicht erkennen, dass Letzterer wesentlich mehr Dokumentation benötigt und alles , was Sie dokumentieren, eine Verpflichtung ist, die Grenzen setzt Ihre weiteren Möglichkeiten.
Ein Beispiel für die Ablehnung eines solchen zweiseitigen Vertrags ist der Wechsel von
show
undhide
nachsetVisible(boolean)
in java.awt.Component .quelle
show
undhide
es gibt sie immer noch, sie sind einfach@Deprecated
. Die Änderung unterbricht also keinen Code, der sie lediglich aufruft. Wenn Sie sie jedoch überschrieben haben, werden Ihre Überschreibungen nicht von Clients aufgerufen, die auf das neue "setVisible" migrieren. (Ich habe noch nie Schaukel verwendet, so dass ich weiß nicht , wie üblich ist es diejenigen außer Kraft zu setzen, aber da es vor langer Zeit passiert ist , stelle ich mir den Grund , dass Deduplicator erinnert es ist , dass es biss ihn / sie schmerzlich.)Kilian Foths Antwort ist ausgezeichnet. Ich möchte nur das kanonische Beispiel * hinzufügen, warum dies ein Problem ist. Stellen Sie sich eine Integer-Point-Klasse vor:
Lassen Sie es uns als 3D-Punkt unterteilen.
Super einfach! Verwenden wir unsere Punkte:
Sie wundern sich wahrscheinlich, warum ich ein so einfaches Beispiel poste. Hier ist der Haken:
Wenn wir den 2D-Punkt mit dem äquivalenten 3D-Punkt vergleichen, erhalten wir Wahr, aber wenn wir den Vergleich umkehren, erhalten wir Falsch (weil p2a fehlschlägt
instanceof Point3D
).Fazit
Es ist normalerweise möglich, eine Methode in einer Unterklasse so zu implementieren, dass sie nicht mehr mit den Erwartungen der Superklasse kompatibel ist.
Es ist im Allgemeinen unmöglich, equals () in einer signifikant anderen Unterklasse auf eine Weise zu implementieren, die mit der übergeordneten Klasse kompatibel ist.
Wenn Sie eine Klasse schreiben, deren Unterklassen Sie zulassen möchten, empfiehlt es sich, einen Vertrag für das Verhalten der einzelnen Methoden zu erstellen. Noch besser wäre eine Reihe von Unit-Tests, mit denen die Leute ihre Implementierung überschriebener Methoden testen könnten, um zu beweisen, dass sie nicht gegen den Vertrag verstoßen. Fast niemand macht das, weil es zu viel Arbeit ist. Aber wenn es dich interessiert, ist es das, was du tun musst.
Ein gutes Beispiel für einen gut formulierten Vertrag ist Comparator . Ignorieren Sie einfach, worüber es
.equals()
aus den oben beschriebenen Gründen sagt . Hier ist ein Beispiel dafür, wie Comparator das nicht.equals()
kann .Anmerkungen
Josh Blochs "Effective Java" Item 8 war die Quelle dieses Beispiels, aber Bloch verwendet einen ColorPoint, der anstelle einer dritten Achse eine Farbe hinzufügt und anstelle von Ints Doubles verwendet. Das Java-Beispiel von Bloch wird im Wesentlichen von Odersky / Spoon / Venners dupliziert , die ihr Beispiel online zur Verfügung gestellt haben.
Mehrere Personen haben Einwände gegen dieses Beispiel erhoben, da Sie dieses Problem beheben können, wenn Sie die übergeordnete Klasse über die Unterklasse informieren. Das ist wahr, wenn es eine ausreichende Anzahl von Unterklassen gibt und wenn die Eltern über alle Bescheid wissen. Die ursprüngliche Frage war jedoch, eine API zu erstellen, für die jemand anderes Unterklassen schreibt. In diesem Fall können Sie die übergeordnete Implementierung im Allgemeinen nicht so aktualisieren, dass sie mit den Unterklassen kompatibel ist.
Bonus
Comparator ist auch deshalb interessant, weil es darum geht, equals () korrekt zu implementieren. Besser noch, es folgt einem Muster zur Behebung dieser Art von Vererbungsproblemen: dem Strategieentwurfsmuster. Die Typeclasses, auf die sich Haskell und Scala freuen, sind auch das Strategiemuster. Vererbung ist nicht schlecht oder falsch, sie ist nur knifflig. Für weitere Lesung Besuche Philip Wadler Papier Wie Ad-hoc - Polymorphismus weniger ad hoc machen
quelle
equals
Map und Set. Gleichheit ignoriert die Reihenfolge vollständig, so dass beispielsweise zwei SortedSets mit denselben Elementen, aber unterschiedlichen Sortierreihenfolgen immer noch gleich verglichen werden.B
ein Kind einer Klasse istA
und Sie ein Objekt einer Klasse instanziieren, sollten SieB
das KlassenobjektB
in das übergeordnete Objekt umwandeln und die API der umwandelten Variablen verwenden können, ohne Implementierungsdetails von zu verlieren das Kind. Sie haben die Regel verletzt, indem Sie die dritte Eigenschaft bereitgestellt haben. Wie planen Sie, auf diez
Koordinate zuzugreifen , nachdem Sie diePoint3D
Variable in umgewandelt habenPoint2D
, wenn die Basisklasse keine Ahnung hat, dass eine solche Eigenschaft vorhanden ist? Wenn Sie durch das Umwandeln einer untergeordneten Klasse in die Basis die öffentliche API beschädigen, ist Ihre Abstraktion falsch.Vererbung schwächt die Kapselung
Wenn Sie eine Schnittstelle veröffentlichen, deren Vererbung zulässig ist, erhöhen Sie die Größe Ihrer Schnittstelle erheblich. Jede überschreibbare Methode könnte ersetzt werden und sollte daher als Rückruf für den Konstruktor betrachtet werden. Die von Ihrer Klasse bereitgestellte Implementierung ist lediglich der Standardwert des Rückrufs. Daher sollte ein Vertrag abgeschlossen werden, aus dem die Erwartungen an die Methode hervorgehen. Dies kommt selten vor und ist ein Hauptgrund, warum objektorientierter Code als spröde bezeichnet wird.
Nachfolgend finden Sie ein reales (vereinfachtes) Beispiel aus dem Java-Kollektions-Framework, mit freundlicher Genehmigung von Peter Norvig ( http://norvig.com/java-iaq.html ).
Was passiert also, wenn wir dies unterordnen?
Wir haben einen Bug: Hin und wieder fügen wir "dog" hinzu und die Hashtabelle bekommt einen Eintrag für "dogss". Die Ursache war, dass jemand eine Implementierung von put bereitstellte, die die Person, die die Hashtable-Klasse entwarf, nicht erwartet hatte.
Vererbung unterbricht die Erweiterbarkeit
Wenn Sie zulassen, dass Ihre Klasse in Unterklassen unterteilt wird, verpflichten Sie sich, Ihrer Klasse keine Methoden hinzuzufügen. Dies könnte sonst geschehen, ohne etwas zu zerbrechen.
Wenn Sie einer Schnittstelle neue Methoden hinzufügen, muss jeder, der von Ihrer Klasse geerbt hat, diese Methoden implementieren.
quelle
Wenn eine Methode aufgerufen werden soll, müssen Sie nur sicherstellen, dass sie ordnungsgemäß funktioniert. Das ist es. Getan.
Wenn eine Methode überschrieben werden soll, müssen Sie auch den Bereich der Methode sorgfältig abwägen: Wenn der Bereich zu groß ist, muss die untergeordnete Klasse häufig kopierten Code aus der übergeordneten Methode einschließen. Wenn es zu klein ist, müssen viele Methoden überschrieben werden, um die gewünschte neue Funktionalität zu erhalten. Dies erhöht die Komplexität und die unnötige Zeilenanzahl.
Daher muss der Ersteller der übergeordneten Methode Annahmen treffen, wie die Klasse und ihre Methoden in Zukunft möglicherweise überschrieben werden.
Der Autor spricht jedoch über ein anderes Thema im zitierten Text:
Betrachten Sie eine Methode,
a
die normalerweise von method aufgerufen wirdb
, in einigen seltenen und nicht offensichtlichen Fällen jedoch von methodc
. Wenn der Autor der überschreibenden Methode diec
Methode und ihre Erwartungen überblickta
, ist es offensichtlich, wie etwas schief gehen kann.Deshalb ist es wichtiger, dass
a
klar und eindeutig definiert, gut dokumentiert, "eins und das auch gut" ist - mehr als wenn es eine Methode wäre, die nur zum Aufrufen gedacht wäre.quelle