Flexibel programmieren mit dem Fluent Interface
Seite 3: Umgangssprache
Zartschmelzende API gefährdet Konsistenz
Stattdessen lässt sich ein Abschnitt auch folgendermaßen konstruieren:
Abschnitt abschnitt =
Abschnitt.mitNamen("abschnittsname").mitWert("a").anStelle(1).undMitWert("b").a
nStelle(2);
Diese Konstruktion liest sich wie die umgangssprachliche Formulierung oben, lediglich ein paar sprachbedingte Punktionen stören noch die freie Rede. Diese Art der Erzeugung gegen eine Schnittstelle, die solch eine flüssige Formulierung zulässt, nennt sich Fluent Interface. Es ermöglicht dem Entwickler den Sprung von einer API hin zu einer DSL, einer Domain Specific Language [b, c], die die Konstruktion von Objekten für den Anwender einer Schnittstelle stark vereinfacht. Martin Fowler und Eric Evans haben diesen Begriff geprägt und das Pattern herausgearbeitet. Die flüssigen Schnittstellen sind stark im Domain Driven Design [1] vertreten, ihre Wurzeln reichen jedoch deutlich weiter zurück: Smalltalker und Lispler sind in der Regel damit vertraut. Es gibt aber auch in modernen Programmiersprachen mittlerweile gute Beispiele für die Anwendung dieses Pattern, etwa Java-Bibliotheken wie JUnit, Easymock und JMock oder die Web-Frameworks Ruby on Rails und Grails.
Die Implementierung einer flüssigen Schnittstelle ist ungleich anspruchsvoller als die einer klassischen, festen. Ein naiver Ansatz, der – euphemistisch gesehen – eher zartschmelzend als flüssig daherkommt, sähe aus wie Listing 2.
Damit kann ein Benutzer einen Abschnitt schon flüssig konstruieren. Es fällt zunächst auf, dass Setter-Methoden einen Rückgabewert haben können, hier jeweils das zu konstruierende Objekt. Erst diese Eigenschaft der Setter einer flüssigen API ermöglichen eine Aneinanderreihung von Methodenaufrufen und damit einen grammatikalisch korrekten Satzbau.
Hinzu kommt, dass diese Notation von der in Java verbreiteten Konvention abweicht, für Methoden Befehle in Form von Imperativen zu verwenden, etwa initialisiere()
, add()
oder getFoo()
. Der Vorteil der Konvention ist, dass beim Blick auf die an einem Objekt aufzurufenden Methoden anhand des Methodennamens klar wird, welche Befehle das Objekt entgegennehmen kann.
Bei einem flüssigen Interface gibt es viele Methoden, deren Namen wenig Aussagekraft über ihre Verwendbarkeit haben. Beispiele dafür sind mit
, in
, undMit
oder anStelle
. Diese Methoden sind nur aus dem Kontext, also dem Satzbau, heraus interpretierbar. Es ist zwar recht einfach, flüssige Schnittstellen als solche aufgrund der Methodennamen zu identifizieren und sich als Programmierer von der vorgegebenen Grammatik leiten zu lassen, jedoch steigt der Schwierigkeitsgrad mit der Größe der Schnittstelle.
Lesbarer ist die Objektkonstruktion mittlerweile und flexibler ebenfalls: Die Methoden lassen sich beliebig miteinander verknüpfen. Neue Attribute am Objekt kann man quasi sofort bei der Konstruktion verwenden und muss sie nicht erst in die Konstruktoren einarbeiten. Allerdings schießt diese Flexibilität weit übers Ziel hinaus, denn wenn die Methoden beliebig miteinander verknüpfbar sind, ist keine Konsistenz des konstruierten Objekts zu gewährleisten:
Abschnitt abschnitt =
Abschnitt.mitNamen("abschnittsname").mitWert("a").mitWert("b").anStelle(2);
Offensichtlich hat der Konstrukteur hier vergessen, den Wert a
an die Stelle 1
zu schreiben. Das Verhalten dieses halbfertigen Objekts ist nicht ersichtlich, ohne den Quellcode der Abschnittsklasse zu studieren. Dies jedoch sollte wiederum für eine gute API unnötig sein; selbsterklärende Schnittstellen erhöhen ihre Benutzbarkeit enorm.
Leider konnte die zartschmelzende Variante die Unübersichtlichkeit der festen API nicht beseitigen. Hierzu ein Blick auf die Code-Vervollständigung: Eine IDE bietet bei der Code Completion auf eine Tastenkombination hin all die Codestücke wie Variablen, Methoden und Klassen an, die an der Stelle, an der sich der Cursor befindet, sinnvoll sind. Insbesondere dient dies dazu, an Objekten diejenigen Methoden ausfindig zu machen, die an ihm aufgerufen werden können. So lässt sich schnell durch eine API navigieren und Code niederschreiben.
Abschnitt mitNamen(String name)
ist die einzige statische Methode, die durch Code-Vervollständigung an der Abschnittsklasse angeboten wird, sodass der Benutzer nichts falsch machen kann. Die zur Konstruktion von Abschnitten gedachten Objekt-Methoden dagegen sind Abschnitt mitWert(String wert)
, undMitWert(String wert)
sowie Abschnitt anStelle(int stelle)
. Es ist jetzt für den Benutzer nicht ersichtlich, welche Methoden er aufrufen darf (mitWert()
sowie undMitWert()
) und welche nicht (anStelle()
). Selbst die Reihenfolge der Code-Aufrufe könnte vertauscht werden: Statt .mitWert("a").anStelle(1)
könnte der Entwickler .anStelle(1).mitWert("a")
schreiben – und sich zu Recht wundern, warum er keinen brauchbaren Abschnitt erzeugen konnte. Hier hilft ihm auch die deutsche Grammatik nicht weiter, denn die Sätze "Abschnitt mit Wert a an Stelle 1" und "Abschnitt an Stelle 1 mit Wert a" folgen beide einem legitimen Satzbau.