Hinweis:
Dieser Blogartikel ist älter als 5 Jahre – die genannten Inhalte sind eventuell überholt.
Der Einsatz von Fluent Interfaces und Method Chaining erfreut sich großer Beliebtheit und findet immer mehr Einzug in die APIs aktueller Produkte und Bibliotheken. Solche Funktionalitäten zu implementieren kann aber mitunter recht komplex sein und kollidiert mit manchen althergebrachten Paradigmen und Best-Practices. Dieser Beitrag beschreibt die Probleme, die vor allem im Kontext von Vererbung und Polymorphie auftreten.
Vererbung und Polymorphie
Die Konzepte der Vererbung und der damit einhergehenden Polymorphie sind für Entwickler, die Wert auf Objektorientierung legen, seit langer Zeit ganz selbstverständliche Werkzeuge, um Wiederverwendung zu erreichen, die verhasste Duplikation bei der Entwicklung zu vermeiden und damit die Wartbarkeit des Codes zu erhalten und zu verbessern – zumindest bei der Arbeit mit Java oder anderen Sprachen mit einem Schwerpunkt auf OO.
Allerdings gibt es viele Beispiele, bei denen die entstandenen Superklassen mehr einem Sammelkasten nützlicher Funktionen für alle ihre Subklassen ähneln und der Aspekt der Polymorphie dabei viel zu kurz kommt. Ähnlich wie bei der Entwicklung gegen Schnittstellen soll man auch hier davon profitieren, dass Instanzen von Subklassen überall dort eingesetzt werden können, wo ihre Superklassen erwartet werden. Und wenn es nur um die Wiederverwendbarkeit von Funktionen geht, so bietet sich die Delegation als passende Alternative zur Vererbung an. Dabei ist es jedem überlassen, ob adhoc-Instanzen oder statische Methoden an abstrakten Helferklassen bevorzug werden.
Erweiterung von Klassen mit Fluent Interface
Nun bin ich kürzlich auf ein Problem gestoßen, bei dem ich mit einem Fluent Interface arbeiten wollte, aber die gewohnte Nutzung von Polymorphie nicht so recht funktioniert hat.
Ich habe es mit mehreren Klassen zu tun, die alle bestimmte Werte als Strings serialisieren sollen und dafür jeweils einen StringBuilder
verwenden. Ähnlich dem ToStringHelper
der guava Bibliothek sollen die Werte jeweils mit einer Beschreibung und bestimmten Verbindungszeichen aufgelistet werden, aber jeweils nur, wenn sie nicht Null sind. Dafür gibt es eine Methode in einer gemeinsamen Superklasse, welche die Beschreibung, den Wert und den StringBuilder
entgegennimmt.
1 2 3 4 5 6 7 8 9 10 11 |
StringBuilder builder = new StringBuilder(); builder.append("overall description"); // hier wird das Fluent Interface benutzt addValueIfNotNull("first description", firstValue, builder); // hier wird die Funktion der Oberklasse benutzt ... addValueIfNotNull("last description", lastValue, builder); String result = builder.toString(); |
Neben den Werten, die von der addValueIfNotNull()
Methode eingefügt werden, gibt es teilweise noch einige direkte Aufrufe am Fluent Interface des StringBuilder
und ich finde, dass es besser wäre, dieses Konzept hier durchgängig anzuwenden. Also ist der naheliegende Schritt eine Erweiterung der Klasse StringBuilder
, um die Methode appendIfNotNull(String caption, Object value)
in das Fluent Interface zu integrieren und intern die vielen überschriebenen append(type)
Methoden zu nutzen, die bereits vorhanden sind. Natürlich soll dieser erweiterte StringBuilder
auch weiterhin überall eingesetzt werden können, wo bisher mit StringBuilder
-Instanzen gearbeitet wurde. Vererbung und Polymorphie sollten das ja ohne Weiteres ermöglichen.
StringBuilder kann nicht erweitert werden
Leider musste ich feststellen, dass die Klasse StringBuilder
als final deklariert ist. Es gibt zwar noch eine abstrakte Superklasse (die auch von StringBuffer
erweitert wird) aber diese ist ebenfalls nicht zweckdienlich.
Bei genauerer Betrachtung wird klar, warum der StringBuilder
final ist, denn wie ich schon bei der Arbeit mit dem Builder Pattern gelernt habe, lässt sich ein Fluent Interface nur sehr schlecht mit Vererbung kombinieren und führt zu einer nicht ohne weiteres erweiterbaren Typhierarchie. In diesem Fall geht es um ein naives Fluent Interface, also ohne Grammatik, bei dem alle dafür relevanten Methoden immer denselben Typ zurückliefern. Zusätzliche Methoden (z.B. long StringBuilder.length()
) lassen wir hier außen vor. Jede Methode des StringBuilder
liefert auch einen solchen zurück. Würde man jetzt append(String)
an einem CustomStringBuilder
aufrufen, käme kein CustomStringBuilder
mehr zurück und die zusätzlichen Methoden wären nicht mehr verfügbar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class CustomStringBuilder extends StringBuilder { // nur hypothetisch, faktisch nicht möglich public CustomStringBuilder appendValueIfNotNull(String caption, Object value){ if(value != null) { append(caption).append(":").append(value).append("\n"); // hier kommt StringBuilder zurück } return this; } } new CustomStringBuilder() .appendValueIfNotNull("description",value) .append("\n") .appendValueIfNotNull("description",value) // Fehler, denn .append() hat StringBuilder geliefert. .toString(); |
Subklasse als Typparameter in allen Superklassen
Um die Methoden einer Superklasse so zu gestalten, dass sie wieder den Typ der konkreten Subklasse zurückliefern, muss man dem Curiously-Recurring Generics Pattern folgen. Dabei erhalten alle Superklassen einen Typparameter F, der von allen konkreten Subklassen mit sich selbst belegt werden muss. Dann kann die Signatur der Superklassen diesen Typparameter als Rückgabetyp verwenden. Zusätzlich kann man bereits eine Methode F getThis()
bereitstellen, in der ein ungeprüfter Cast der aktuellen Instanz auf F durchgeführt wird. Eine Konsequenz davon ist, dass alle gemeinsamen Superklassen in einer solchen Typhierarchie abstrakt sein müssen und alle konkreten Subklassen final. Leider bietet die Sprachdefinition von Java keine Möglichkeit wirklich sicherzustellen, dass alle abstrakten Klassen einer Typhierarchy den Typparameter an ihre Superklassen durchreichen und alle konkreten Subklassen ihn auch wirklich mit sich selbst belegen und nicht mit einer anderen Subklasse.
Da nun keine Vererbungsbeziehung zwischen den konkreten Subklassen bestehen kann, ist der Einsatz von Polymorphie hier etwas eingeschränkt. Man kann also vornehmlich die Wiederverwendung gemeinsamer Methoden durch Vererbung erreichen, aber nicht die Erweiterung von bereits instanziierbaren Klassen wie dem StringBuilder
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// F muss von allen konkreten Subklassen mit sich selbst belegt werden, was nicht erzwingbar ist abstract class FluentBase<F extends FluentBase<F>> { private String value; private F getThis() { return (F) this; // Warnung: unchecked cast } public F withValue(String value) { this.value = value; return getThis(); } } final class Fluent extends FluentBase<Fluent> { // nur eine instantiierbare Klasse, um die Funktion der Superklasse nutzbar zu machen. } // hat keine Typ-Beziehung zu Fluent final class ExtendedFluent extends FluentBase<ExtendedFluent> { public ExtendedFluent withExtendedValue(String caption, String value) { return withValue(caption + ":" + value + "\n"); // liefert durch die Superklasse den Typ ExtendedFluent zurück } } |
Ganz davon abgesehen, dass die Klassenhierarchie des StringBuilder
in der Java Runtime nicht in dieser Form vorliegt, hätte ich auch nicht die Möglichkeit gehabt, StringBuilder
als abstrakt zu erklären und eine neue Ebene mit zwei neuen konkreten Subklassen einzuarbeiten. Außerdem ist damit immer noch keine Polymorphie zwischen StringBuilder
und CustomStringBuilder
möglich.
Letztlich bleibt in diesem Fall nur die Delegation von einem CustomStringBuilder
zu einer internen StringBuilder
-Instanz ohne jede Typbeziehung als Lösung. Solange ich dieses Objekt nur lokal verwende und nicht an andere Akteure übergebe, die einen StringBuilder
erwarten, kann ich wenigstens ein ununterbrochenes Fluent Interface genießen.
We’re hiring!
Tapetenwechsel gefällig? Wir sind auf der Suche nach begeisterten Software-Entwicklern, die uns im Umfeld von Java, .NET und JavaScript unterstützen und auch von extravaganteren Sprachen wie Go, Elixir und Clojure nicht zurückschrecken. Jetzt Bewerben!
Weiterlesen
Weitere Information zu unseren Leistungen gibt es auf unserer Website, der direkte Kontakt zu uns ist telefonisch unter +49 721 619 021-0 oder jederzeit per E-Mail an list-blog@inovex.de möglich.