Builder mit Interfaces (Draft)
Reihenfolge der Methoden-Ausführung
Normalerweise sollte die Reihenfolge in der die Modifier-Methoden aufgerufen werden keine Rolle spielen. Sofern das eine Frage der Lesbarkeit ist -- so sollte zuerst die Straße und dann die Hausnummer angegeben werden -- läßt sich das in der Reegel organisatorisch -- also per Handbuch -- steuern lassen, falls es sich nicht (wie bei der Hausnummer) von selbst regelt.
Man kann das Problem zur Laufzeit entweder mit Prüfungen und Exceptions behandeln und dabei das Risiko eingehen damit in die Produktion zu gehen wenn die Tests lückenhaft sind. Falls ein Modifier auf die Erfassung der Daten durch einen anderen Modifier angewiesen ist, kann man das Problem mit Buffering und verzögerter Auswertung umgehen (-> Beispiel??).
Wäre aber nicht eine Lösung wünschenswert, die Probleme zur Compile-Zeit erkennt und es damit unmöglich macht, daß diese in die Produktion gelangen? Im Prinzip ja, leider gehen solche Maßnahmen in der Regel mit erhöhtem Aufwand einher; man tut also gut daran genau zu überlegen wann sich der Aufwand lohnt.
Interfaces, Interfaces
Eine relativ komfortable Lösung bieten Interfaces, sor allem deshalb, weil man sie nachträglich hinzufügen kann. Wir nehmen einfach das obige Beispiel und bauen eine einfachen Builder:
public class AdressBuilder { Adresse adr = new Adresse(); public static AdressBuilder neu() { return new AdressBuilder(); } public AdressBuilder withStrasse(String strasse) { adr.setStrasse(strasse) return this; } public AdressBuilder withHausnummer(int nr) { adr.setHausnummer(nr); return this; } public Adresse get() { return adr; } }
Bei der Verwendung des Builders können wir eine Reihe von unerwünschten Dinge treiben:
- die Modifier in beliebiger Reihenfolge aufrufen
- den Aufruf eines Modifiers auslassen
- einen Modifier mehrfach aufrufen
Jede der drei Situationen läßt sich zur Compile-Zeit verhindern, indem wir drei Interfaces einführen
und über diese die Reihenfolge steuern. Zur Verdeutlichung -- und zur vereinfachung -- numerieren
wir die Interfaces einfach durch. Wir beginnen mit dem ersten Interface für die Methode withStrasse
:
public interface First { Second strasse(String strasse); }
Das Interface First
-- wie das in den Builder eingebaut wird, sehen wir nachher -- erlaubt ausschließlich die Angabe der Straße.
Anschließend geht's weiter mit dem Interface Second
:
public interface Second { Finish hausnummer(int nummer); }
das -- ausschließlich -- die Angabe der Hausnummer erlaubt. Danach könnten in dieser Vorgehensweise weitere Interfaces folgen,
für unser Beispiel ist die Reise vorbei und wir enden mit dem Interface Finish
:
public interface Finish { Adresse get(); }
das nur noch den Aufruf der Generator-Methode erlaubt. Wir halten schonmal fest: Für die Festlegung der Reihenfolge von n Methoden benötigen wir n + 1 (winzige) Interfaces. Bauen wir die Interfaces nun in unseren Builder ein:
public class AdressBuilder implements First, Second, Finish { Adresse adr = new Adresse(); public static First neu() { return new AdressBuilder(); } @Override public AdressBuilder withStrasse(String strasse) { adr.setStrasse(strasse) return this; } @Override public AdressBuilder withHausnummer(int nr) { adr.setHausnummer(nr); return this; } @Override public Adresse get() { return adr; }
}
Wir haben am Builder tatsächlich nur zwei Änderungen vorgenommen:
Zunächst weisen wir den Builder n, die drei Interfaces zu implementieren.
In der Folge erhalten die Methoden die Annotation @Override
.
Die einzige Stelle an der eines der Interfaces -- das erste -- eingetragen werden muß ist an der
Creation-Methode. Wir haben hierfür eine statische Methoden neu
definiert, denn aus offensichtlichem Grunde
funktioniert die Interface-Lösung nicht, wenn Konstruktoren verwendet werden. Dadurch wird erreicht,
daß zu Anfang nur die Methode withStrasse
zur Verfügung steht, der Anfang der Kette.
Wie die Kette funktionert läßt sich am einfachsten in der IDE ausprobieren. Man tippt die Folge
AdressBuilder.neu().strasse("").hausnummer(0).get();
ein und schaut sich nach jedem Punkt die möglichen anwendbaren Methoden an. Neben den unvermeidlichen
Object
-Methoden steht immer nur eine Methode des AdressBuilder
zur Auswahl.
Optionale Aufrufe
Bis hierher ist der Aufruf jeder der Builder-Methoden zwingend erforderlich.
Nehmen wir an, die Angabe der Hausnummer sei optional und die entsprechende Methode soll übersprungen werden können.
Dazu müssen wir dem entsprechenden Interface (Second
) die zusätzliche Möglichkeit geben die Methode get
aufzurufen.
Das können wir so machen:
public interface First{ Finish hausnummer(int nummer); Adresse get(); }
Damit implementiert die Methode get
im AdressBuilder
nun zwei Interfaces.
Nachteil dieser Lösung ist, daß die beiden Interfaces synchron gehalten werden müssen. Das iat
normalerweise kein Problem, weil sich der Typ des Builder-Generats in der Regel nicht ändert.
Aber es dupliziert eine Zeile und für jede optionale Modifier-Methode müssen wir die Zeile wiederholen.
Hier also die Alternative. Dafür modifizieren wir das Interface First
:
public interface First { <T extends Second & Finish> T strasse(String strasse); }
Statt im Interface Second
anzugeben daß das Interface übersprungen werden kann,
geben wir nun an der Methode withStrasse
an, welche Interfaces im Anschluß angewandt werden können.
Ein feiner aber wichtiger Unterschied im Design.
Java bietet uns hier durch die Forderung der Typ T
müsse zwei Interfaces implementieren
eine Alternative zur nicht vorhandenen Mehrfachvererbung. Die Implementierung der Methode muß ein Objekt
im Ergebnis liefern, das beide Interfaces implementiert. AdressBuilder
tut das:
public class AdressBuilder implements First, Second, Finish { ... @Override public AdressBuilder withStrasse(String strasse) { adr.setStrasse(strasse) return this; }
Der Compiler sieht dabei allerdings ein Problem und warnt uns davor, daß hier eine ungeprüfter Typ-Konversion stattfindet obwohl das offensichtlich kein Problem darstellt...
Den Effekt kann man in der IDE wieder sehr schön nachvollziehen.
Alternativen
Anstelle der Straße möchten wir gerne ein Postfach angeben können. In diesem Falle sind weder Straße noch Hausnummer sinnvoll. Dazu erweitern wir zunächst den Builder um eine Modifier-Methode:
@Override public AdressBuilder withPostfach(int postfach) { adr.setPostfach(postfach); return this; }
Die IDE wird dann freundlicherweise das Interface First
für uns erweitern und wir müssen nur die Signatur anpassen:
Finish withPostfach(int postfach);
Da hinter der Hausnummer nichts mehr kommt, gehen wir hier gleich zur Generator-Methode -- sprich zu Interface Finish
über.
Die Verweendung eines Interface anstelle der Generator-Methode verschafft uns hier noch einen weiteren Vorteil.
Wenn wir nun eine weitere Modifier-Methode -- etwa für den Ort -- mit einem weiteren Interface einführen,
schiebt sich eine weitere Ebene zwischen First
und die Generator-Methode mit einem weiteren Interface das Third
heißen möge.
Dafür brauchen wir dann nur den Ergebnis-Typ der withPostfach
-Methode auf Third
zu ändern.
Wir gewinnen also Einheitlichkeit und damit Änderungsfreundlichkeit.
Ausblick
Abschließend stellt sich die Frage, was für Strukturen wir auf diese Weise modellieren können? Ohne den formalen Beweis zu erbringen liegt die Vermutung nahe, daß jede Methoden-Kette die sich als endlicher Automat darstellen läßt mit Hilfe der Interface-Methode modellieren läßt. Die Knoten des Automaten bilden die Interfaces und die Kanten die Modifier-Methoden.
was nicht geht
Was uns zu der Frage nach den Grenzen der Methode führt. In der Gruppe Straße/Postfach haben wir eine Alternative programmiert. Wollten wir in der Gruppe A/B zusätzlich die Variante "A und B" zulassen, müssen wir das über ein separates Interface machen. Andernfalls müssen wir entweder die Variante "weder A noch B" oder die Mehrfach-Anwendung von A bzw. B zulassen.
Abgrenzung zu kontextfreien Sprachen... Folgen... Klammerungs-Äquivalent geht nicht