Die Frage, die sich vielen Entwicklern gar nicht stellt, ist die: Wann verwende ich direkte Vererbung (extends) und wann indirekte Vererbung (delegate)?
Nun vorneweg möchte ich erläutern, dass dies auf den ersten Blick, gleich zu sein scheint, um dann im darauffolgenden Abschnitt die Unterschiede näher zu beleuchten.
Anschließend sollte die Frage, wann was zu verwenden ist, leichter beantwortet werden können.
Die Clean Code Developer haben das Prinzip „Favour composition over inheritance“, was soviel bedeutet wie, sich im Zweifelsfall für Aggregation/Komposition anstelle direkter Vererbung zu entscheiden.
Als motivierendes erste Beispiel dient der gute alte Pinguin:
public class Vogel { public void fressen() {...} public void singen() {...} public void fliegen() {...} } public class Sittich extends Vogel { // Vielleicht nichts zu tun. Perfekt! } public class Pinguin extends Vogel { // Aber jetzt haben wir ein Problem: // Pinguine können nicht fliegen!!! }
An dieser Stelle hilft das FCoI Prinzip sehr schnell, denn das Problem ist „Pinguine können nicht fliegen“, also gibt es keine wirklich sinnvolle Implementierung.
Auch das Werfen einer OperationNotAllowedException ist nur suboptimal.
Wir könnten also unterscheiden zwischen FlugVögeln und NichtFlugVögeln und entsprechende Vererbungshierarchien einbauen. Wenn nun ein staatlich geprüfter Ornithologe neben uns säße,
würde er uns sicherlich warnen, dass die Welt der Vögeln eben vielschichtiger ist und unsere Hierarchie noch lange nicht ausreicht.
Ähnliche Erfahrungen dürfte man auch in entsprechenden Projekten machen. Eine simple Ausgangssituation kann ich zwei, drei Sprints zu einem undurchsichtigen Zusammenspiel unterschiedlicher Funktionalitäten und Subtypen werden. Wer also hilft einem da den Überblick zu bewahren?
Meiner Meinung nach empfiehlt es sich eben genau diese Eigenschaft als Funktionalitäten auszulagern, die man in die entsprechenden Vogelarten injiziert:
public interface FlugFaehigkeit { void macheFlug(); } public class Flattern implements FlugFaehigkeit { public void machFlug() {...} } public interface Vogel { } public interface FlugVogel extends Vogel { void fliegen(); } public class Sittich implements Vogel { private FlugFaehigkeit flugtechnik = new Flattern(); public void fliegen() { flugtechnik.macheFlug(); } }
Nun habe ich ja doch eine Verbungshierarchie gebaut, wird der aufmerksame Leser sagen.
Ja, aber eben Vererbung durch Interfaces und die geben mir letztendlich die Schnittstelle vor, die ich durch meinen Entwurf haben möchte. Nicht mehr, aber auch nicht weniger.
Um die gemeinsam genutzte Implementierung auch im besten Sinne wiederverwenden zu können, nutze ich hier die Klasse FlugFaehigkeit bzw. deren Implementierungen. Mein beliebtestes Beispiel hierfür ist das Auto, das eben fahren kann, weil Räder rollen. Definitiv ist ein Auto KEIN Rad.
Zudem untersützt dieses Prinzip auch meinen Favoriten: Separation of concerns.
Die Funktionalitäteninterfaces und ihre Implementierung erlauben mir die einzelnen Funktionen völlig losgelöst voneinander zu testen und zu implementieren. Das ermöglicht einfachere Erweiterungen und Modifikationen ohne immer die gleiche Klasse, nämlich den Sittich, bearbeiten zu müssen. Gleichzeitig implementiert der Siuttich das Interface Vogel, so dass ich auch mit generischen Lösungen und Listen von Vögeln arbeiten kann.
Dennoch heißt das Prinzip Favour composition over inheritance.
Es gibt also Stellen, an denen Aggregation und/oder Komposition nicht das Mittel der Wahl ist.
Ein solches Beispiel findet sich im Umfeld der hybris Entwicklung.
Das eCommerce Framework hybris bietet im besten Sinne eine serviceorientierte Architektur. Aufsetzend auf dem Service Layer der hybris Plattform können Client-Anwendungen, wie beispielsweise der hausinterne Accelerator, im eCommerce Bereich Geschäftsprozesse abbilden.
So gibt es beispielsweise in hybris den CommerceCartService (als Interface) und eine Standardimplementierung, den DefaultCommerceCartService.
Die Standardimplementierung eines durch das Produkt hybris vorgegebenen Service ist nicht immer an allen Stellen durch Aufrufe geeigneter Strategien erweiterbar, so wie es beispielsweise beim CommerceCartService der Fall ist. Es kann aber auch den Fall geben, dass Kunde ganz spezielle Wünsche, Anforderungen und Ideen haben, was in den einzelnen Schritten passieren soll, so dass man die Standardimplementierung überschreiben muss.
In diesem Fall ist von einer Aggregation des DefaultCommerceCartService und einer Implementierung des Interfaces CommerceCartService abzusehen, wenn man in neueren Versionen des Produkts hybris, die neuen Methoden des DefaultCommerceCartService OOTB erhalten und nutzen können möchte. Eine erneute Delegation an den DefaultCommerceCartService wäre hier nur zusätzlicher Aufwand.
Allerdings gibt es ein noch viel schwerwiegenderes Problem. Der DefaultCommerceCartService besitzt Abhängigkeiten zu anderen Services, die in diesem Umfeld sehr gängig sind, wie beispielsweise den ModelService, FlexibleSearchService, …
Wenn ich nun den DefaultCommerceCartService delegiere, muss ich mich um das Dependency Management dieser typischen CoreServices selbst kümmern und kann insbesondere die in den DefaultCommerceCartService injizierten Sub-Services oder Strategien schlecht steuern. Eigentlich funktioniert dies nur, wenn entsprechende Setter als öffentliche Methoden bereitgestellt werden.
In diesem speziellen Fall, in dem also Singleton Services erweitert werden sollen und man keinen Einfluss auf die Art und Weise hat, wie der Basisservice gebaut wird, empfiehlt sich direkte Vererbung anstelle von Aggregation.