Vererbung, Komposition und Standardmethoden

8

Es wird normalerweise zugegeben, dass das Erweitern von Implementierungen einer Schnittstelle durch Vererbung keine Best Practice ist, und diese Zusammensetzung (z. B. das erneute Implementieren der Schnittstelle von Grund auf neu) ist aufrechterhaltbar.

Dies funktioniert, weil der Schnittstellenvertrag den Benutzer zwingt, alle gewünschten Funktionen zu implementieren. In Java 8 bieten Standardmethoden jedoch ein Standardverhalten, das "manuell" überschrieben werden kann. Betrachten Sie das folgende Beispiel: Ich möchte eine Benutzerdatenbank entwerfen, die die Funktionen einer Liste haben muss. Ich wähle aus Effizienzgründen, es durch eine ArrayList zu unterstützen.

%Vor%

Dies würde normalerweise nicht als große Übung angesehen, und man würde es vorziehen, wenn man tatsächlich die vollen Fähigkeiten einer Liste wünscht und dem üblichen Motto "Komposition über Vererbung" folgt:

%Vor%

Wenn Sie jedoch nicht aufpassen, müssen einige Methoden wie z. B. spliterator () nicht außer Kraft gesetzt werden, da es sich um Standardmethoden der List-Schnittstelle handelt. Der Haken dabei ist, dass die Methode spliterator () von List weitaus schlechter abschneidet als die Methode spliterator () von ArrayList, die für die spezielle Struktur einer ArrayList optimiert wurde.

Dies zwingt den Entwickler zu

  1. Beachten Sie, dass ArrayList eine eigene, effizientere Implementierung von spliterator () besitzt und die Methode spliterator () seiner eigenen Implementierung von List oder
  2. manuell überschreibt
  3. verliert mit der Standardmethode eine Menge Leistung.

Die Frage ist also: Ist es immer noch "so wahr", dass man in solchen Situationen Komposition gegenüber Vererbung bevorzugen sollte?

    
Ulysse Mizrahi 06.07.2015, 09:20
quelle

5 Antworten

6

Bevor Sie sich Gedanken über die Leistung machen, sollten wir immer über die Korrektheit nachdenken, d. h. in Ihrer Frage sollten wir überlegen, was Vererbung anstelle von Delegation impliziert. Dies wird bereits in diesem EclipseLink / JPA-Problem veranschaulicht. Aufgrund der Vererbung funktioniert die Sortierung (das gleiche gilt für die Stream-Operation) nicht, wenn die Liste mit den langsam gefüllten Daten noch nicht gefüllt ist.

Wir müssen also abwägen zwischen der Möglichkeit, dass die Spezialisierungen, die die neuen default -Methoden überschreiben, im Vererbungsfall komplett durchbrechen und der Möglichkeit, dass die default -Methoden nicht mit der maximalen Leistung in der Delegation arbeiten Fall. Ich denke, die Antwort sollte offensichtlich sein.

Da Ihre Frage ist, ob die neuen default Methoden ändern die Situation, sollte betont werden, dass Sie über eine Leistungseinbuße im Vergleich zu etwas sprechen, was noch nicht einmal existierte. Bleiben wir bei sort . Wenn Sie delegation verwenden und die Sortiermethode default nicht überschreiben, hat die default -Methode möglicherweise eine geringere Leistung als die optimierte ArrayList.sort -Methode, aber vor Java 8 war diese nicht vorhanden und ein Algorithmus nicht für% co_de optimiert % war das Standard -Verhalten.

Damit Sie die Leistung nicht mit der Delegierung unter Java 8 verlieren, werden Sie einfach nicht mehr gewinnen, wenn Sie die ArrayList -Methode nicht überschreiben. Aufgrund anderer Verbesserungen, nehme ich an, dass die Leistung immer noch besser ist als unter Java 7 (ohne default Methoden).

Die default API ist nicht leicht vergleichbar, da die API nicht vor Java 8 existierte. Es ist jedoch klar, dass ähnliche Operationen, z.B. Wenn Sie eine Reduktion von Hand implementieren, hatten Sie keine andere Wahl, als die Stream Ihrer Delegationsliste durchzugehen, die gegen Iterator versucht werden musste, also wickeln Sie die remove() ArrayList ein oder verwenden Sie Iterator und size() , die an die Sicherung get(int) delegieren. Es gibt also kein Szenario, in dem eine pre- List -Methoden-API eine bessere Leistung als die default -Methoden der Java 8-API aufweisen könnte, da es in der Vergangenheit ohnehin keine default -spezifische Optimierung gab.

Das heißt, Ihr API-Design könnte durch die Verwendung von komposition auf eine andere Art und Weise verbessert werden, indem Sie ArrayList überhaupt nicht UserDatabase implementieren lassen. Stellen Sie einfach List<User> über eine Accessor-Methode bereit. Dann versucht anderer Code nicht, über die List -Instanz zu streamen, sondern über die Liste, die von der Accessor-Methode zurückgegeben wird. Die zurückgegebene Liste kann ein schreibgeschützter Wrapper sein bietet eine optimale Leistung, wie sie von der JRE selbst bereitgestellt wird, und sorgt dafür, dass die UserDatabase -Methoden, sofern möglich, außer Kraft gesetzt werden.

    
Holger 06.07.2015, 10:30
quelle
2

Ich verstehe das große Problem hier nicht wirklich. Sie können Ihre UserDatabase immer noch mit einer ArrayList sichern, selbst wenn sie nicht erweitert wird, erhalten und die Leistung per Delegierung. Sie müssen es nicht erweitern, um die Leistung zu erhalten.

%Vor%

Deine zwei Punkte ändern das nicht. Wenn Sie wissen, dass "ArrayList eine eigene, effizientere Implementierung von spliterator ()" hat, können Sie es an Ihre Backing-Instanz delegieren, und wenn Sie es nicht wissen, kümmert sich die Standardmethode darum.

Ich bin mir immer noch nicht sicher, ob es wirklich sinnvoll ist, die List-Schnittstelle zu implementieren, es sei denn, Sie erstellen explizit eine wiederverwendbare Collection-Bibliothek. Erstellen Sie besser Ihre eigene API für solche Unikate, die nicht mit zukünftigen Problemen durch die Vererbungs- (oder Schnittstellen-) Kette verbunden sind.

    
oligofren 06.07.2015 09:34
quelle
0

Ich kann keinen Rat für jede Situation geben, aber für diesen speziellen Fall würde ich vorschlagen, das List überhaupt nicht zu implementieren. Was wäre der Zweck von UserDatabase.set(int, User) ? Wollen Sie wirklich den i-ten Eintrag in der Backing-Datenbank durch den komplett neuen Benutzer ersetzen? Was ist mit add(int, User) ? Es scheint für mich, dass Sie es entweder als schreibgeschützte Liste implementieren sollten ( UnsupportedOperationException auf jede Modifikationsanfrage werfen) oder nur einige Modifikationsmethoden unterstützen (wie add(User) wird unterstützt, aber add(int, User) ist nicht). Aber der letztere Fall wäre für die Benutzer verwirrend. Es ist besser, eine eigene Modifikations-API bereitzustellen, die für Ihre Aufgabe besser geeignet ist. Wie bei Leseanfragen wäre es wahrscheinlich besser, einen Strom von Benutzern zurückzugeben:

Ich würde vorschlagen, eine Methode zu erstellen, die Stream zurückgibt:

%Vor%

Beachten Sie, dass Sie in diesem Fall die Implementierung in Zukunft völlig frei ändern können. Ersetzen Sie beispielsweise ArrayList durch TreeSet oder ConcurrentLinkedDeque oder was auch immer.

    
Tagir Valeev 06.07.2015 10:24
quelle
0

Die Auswahl ist basierend auf Ihren Anforderungen einfach.

Hinweis - Das Folgende ist nur ein Anwendungsfall. um den Unterschied zu veranschaulichen.

Wenn du eine Liste willst, die keine Duplikate enthält und eine Menge Dinge macht, die sich sehr von ArrayList unterscheiden, dann ist es nicht sinnvoll, ArrayList zu erweitern, weil du alles von Grund auf neu schreibst.

In dem oben genannten sollten Sie Implement List . Aber wenn Sie nur eine Implementierung von ArrayList optimieren, dann kopieren Sie die gesamte Implementierung von ArrayList und folgen Sie der Optimierung anstatt extending ArrayList . Warum, weil mehrere Ebenen der Implementierung es schwierig für jemanden macht, Dinge zu sortieren.

ZB: Ein Computer mit 4 GB Ram als Eltern und Kind hat 8 GB RAM. Es ist schlecht, wenn der Elternteil 4 GB hat und das neue Kind 4 GB hat, um 8 GB zu machen. Anstelle eines Kindes mit 8 GB RAM-Implementierung.

Ich würde in diesem Fall composition vorschlagen. Aber es wird sich je nach Szenario ändern.

    
Viswanath Lekshmanan 06.07.2015 10:49
quelle
0
  

Es wird normalerweise zugegeben, dass die Erweiterung von Implementierungen einer Schnittstelle durch Vererbung keine Best Practice ist und dass diese Komposition (z. B. die Implementierung der Schnittstelle von Grund auf neu) wartungsfreundlicher ist.

Ich denke nicht, dass das überhaupt richtig ist. Sicherlich gibt es viele Situationen, in denen die Komposition der Vererbung vorzuziehen ist, aber es gibt viele Situationen, in denen Vererbung der Komposition vorgezogen wird!

Es ist besonders wichtig zu erkennen, dass die Vererbungsstruktur Ihrer Implementierungsklassen nicht so aussieht wie die Vererbungsstruktur Ihrer API.

Glaubt wirklich irgendjemand zum Beispiel, dass beim Schreiben einer grafischen Bibliothek wie Java Swing jede Implementierungsklasse die paintComponent () Methode neu implementieren sollte? Tatsächlich besteht ein Hauptprinzip des Designs darin, dass beim Schreiben von Paint-Methoden für neue Klassen superpaint () aufgerufen werden kann und sichergestellt wird, dass alle Elemente in der Hierarchie gezeichnet werden, sowie die Komplikationen bei der Anbindung an die native Schnittstelle auf den Baum.

Allgemein wird angenommen, dass die Erweiterung von Klassen, die nicht unter Ihrer Kontrolle stehen und nicht darauf ausgelegt sind, die Vererbung zu unterstützen, gefährlich ist und potenziell eine Quelle für irritierende Fehler bei der Implementierung darstellt. (Markieren Sie also Ihre Klassen als endgültig, wenn Sie sich das Recht vorbehalten, Ihre Implementierung zu ändern!). Ich bezweifle, dass Oracle bahnbrechende Änderungen in die ArrayList-Implementierung einführt! Wenn Sie seine Dokumentation respektieren, sollte es Ihnen gut gehen ....

Das ist die Eleganz des Designs. Wenn sie zu dem Schluss kommen, dass ein Problem mit der ArrayList vorliegt, schreiben sie eine neue Implementierungsklasse, ähnlich wie sie Vector am Tag ersetzt haben, und es müssen keine einschneidenden Änderungen vorgenommen werden.

================

In Ihrem aktuellen Beispiel lautet die operative Frage: Warum existiert diese Klasse überhaupt?

Wenn Sie eine Klasse schreiben, die die Schnittstelle der Liste erweitert, welche anderen Methoden implementiert sie? Wenn es keine neuen Methoden implementiert, was ist falsch an der Verwendung von ArrayList?

Wenn Sie die Antwort wissen, wissen Sie, was zu tun ist. Wenn die Antwort "Ich möchte ein Objekt, das im Grunde eine Liste ist, aber einige zusätzliche Bequemlichkeit Methoden auf dieser Liste zu betreiben", dann sollte ich Zusammensetzung verwenden.

Wenn die Antwort lautet: "Ich möchte die Funktionalität einer Liste grundlegend ändern", dann sollten Sie Vererbung verwenden oder von Grund auf neu implementieren. Ein Beispiel könnte darin bestehen, eine nicht änderbare Liste zu implementieren, indem Sie die add-Methode von ArrayList überschreiben, um eine Ausnahme auszulösen. Wenn Sie sich mit diesem Ansatz nicht wohl fühlen, können Sie die Implementierung von AbstractList von Anfang an in Betracht ziehen, da es sich um eine exakte Erweiterung handelt, um den Aufwand für die Neuimplementierung zu minimieren.

    
phil_20686 06.07.2015 11:16
quelle