Fluent Interface

Fluent Interfaces[1] (deutsch oft übersetzt mit: „flüssige Schnittstellen“, treffender etwa: „sprechende Schnittstellen“) sind ein Konzept für Programmierschnittstellen in der Software-Entwicklung, bei dessen Befolgung man beinahe in Form von Sätzen natürlicher Sprache programmieren kann. Der danach verfasste Programmcode ist gut lesbar und erleichtert das Verständnis des Programms.

Schnittstellen werden oft falsch verwendet. Fluent Interfaces können zu ihrer richtigen Verwendung anhalten, indem für sie eine Grammatik gewählt wird, die von falschen Verwendungen erkennbar verletzt wird. Es gibt zwei Arten, solche „sprechende Schnittstellen“ zu realisieren, mittels Method Chaining (Methodenketten) oder mittels Nested Functions (eingebetteten Funktionen).

Grundlagen

Als Begründer des Konzepts Fluent Interfaces gelten Eric Evans und Martin Fowler. Bei der Beispielimplementierung des Entwurfsmusters Specification[2] erfanden sie das Konzept, neue Objekte mit Hilfe von Methodenketten auf sprechende Weise zu erstellen.

 Specification colorSpec = new ColorSpecification();
 Specification lengthSpec = new LengthSpecification();
 if(colorSpec.and(lengthSpec).isSatisfiedBy(obj)) {
     ...
 }

Im oberen Beispiel steht in der Bedingung der if-Anweisung ganz ausdrücklich, dass das Objekt obj auf beide Bedingungen getestet wird. Ein weiteres Beispiel ist das sprechende Erstellen eines Datums.

 DateFactory.newDate().year(2009).month(2).day(7);

Anders als bei der Verwendung eines Konstruktors, in dem die Bedeutung der drei numerischen Werte versteckt durch ihre Position im Aufruf gegeben wäre, sieht man hier durch die vorstehenden Methodennamen ausdrücklich, welche Bedeutung die einzelnen Werte haben sollen. Außerdem kann der Entwickler einer solchen Schnittstelle die Reihenfolge einschränken, in der die Methoden aufgerufen werden dürfen. Damit können etwa Methodenaufrufe, die mehrere Parameter erwarten, wesentlich verständlicher geschrieben werden.

Besonders in Evans’ Domain-driven Design spielen Fluent Interfaces eine große Rolle, denn sie dienen ihm dazu, spezifische Eigenschaften aus einer Domäne explizit im Programmcode auszudrücken. Fluent Interfaces gehören damit zu den sogenannten Internen Domänenspezifischen Sprachen,[3] auch als Eingebettete Sprache bezeichnet.[4] Es sind Domänenspezifische Sprachen, die in der Syntax einer Programmiersprache realisiert sind.

Implementierung

Naiv ohne Grammatik

Die Beispielimplementierung von Evans und Fowler für das Entwurfsmuster Specifications war sehr schlicht gehalten. Um eine Methodenkette mit and wie oben zu ermöglichen, wurde dem Interface "Specification" nur die neue Methode and() hinzugefügt.

 public interface Specification {

     Specification and(Specification spec);

     boolean isSatisfiedBy(Object obj);

 }

Bei Aufruf von and() liefert also jede Specification eine weitere, die ihrerseits wiederum aus einem Aufruf der Methode and() stammen kann. Durch diesen naiven Ansatz wird jedoch die Implementierung von Typen um Funktionalitäten angereichert, die ihrem eigentlichen Zweck fernliegen. Der Hauptnachteil ist jedoch, dass Methoden in ganz beliebiger Reihenfolge verkettet werden dürfen.

Mit Grammatik

Häufig spielt die Reihenfolge, in der die Methoden einer Methodenkette aneinander gereiht werden dürfen, eine große Rolle. Das folgende Beispiel zeigt die Verwendung eines Fluent Interfaces, das einem Objekt vom Typ Date einige Tage und Stunden hinzufügt.

Date date = CalendarUtils
        .add(5).days()
        .add(10).hours()
        .to(date);

Würde man, wie im naiven Ansatz, mit jedem Aufruf einer Methode immer den gleichen Typ zurückliefern, dann kann der „Satz“ vorzeitig oder falsch beendet werden, indem nicht alle obligatorischen „Satzglieder“ oder manche mehrfach verkettet werden. Damit die solches ausschließende Grammatik erzwungen wird, muss also jeder Aufruf einer Methode einen anderen Typ zurückgegeben, der nämlich nur die jetzt noch erlaubten Folge-Methoden bereithält. Im folgenden Beispiel sieht man, wie der Aufruf der Methode newDate() von DateUtils zur Rückgabe eines Mediators führt. Dieser hält dann die Folge-Methode add bereit. Der Aufruf der Methode add wiederum führt ebenfalls zur Rückgabe eines neuen Mediator usw.

 public class DateUtils {
    public static Mediator newDate() {
        ...
    }
 }

 public class Mediator {
     public Mediator2 add(int i) {
         ...
     }
 }

 public class Mediator2 {
     public Mediator3 days() {
         ...
     }
 }

...
// possible sentence
DateUtils.newDate().add(5).days(). ...

Bernd Schiffer bezeichnet diese Mediatoren auch als Deskriptoren.[5] Mit obigem Beispiel wird also eine Grammatik realisiert, die genau vorgibt, in welcher Abfolge die Methoden aufgerufen werden können. Außerdem liefert die Methodenkette solange kein gewünschtes Objekt vom Typ Date, wie sie noch nicht vollständig ist. Deshalb zeigen sich bei Verwendung einer so implementierten Klasse DateUtils Fehler schon bei der Kompilierung des anwendenden Programms und nicht erst zur Laufzeit.

Vorteile

Die Vorteile liegen in der leichteren Entwicklung nutzender Programme und der besseren Lesbarkeit des dazu verfassten Programmcodes.

  • Fluent Interfaces können einem natürlich-sprachlichen Satz sehr nahekommen. Damit muss man nur wenig zusätzlich kommentieren.
  • Durch ein satzähnliches Fluent Interface und den damit insinuierten erlaubten Satzaufbau bekommt der Benutzer klarere Vorstellungen über die angebotenen Funktionalitäten und ihren möglichen Gebrauch.
  • Eine Entwicklungsumgebung mit Autovervollständigung wie etwa Eclipse zeigt an, welche nächsten Methoden aufgerufen werden können.

Nachteile

Die Nachteile liegen im Aufwand für das Fluent Interface selbst und der erschwerten Entwicklung von nutzenden Programmen.

  • Die Realisierung einer Grammatik für Fluent Interface ist sehr aufwendig und das notwendige Netzwerk von Mediatoren wird schnell unübersichtlich. Zudem lässt sich auf deren Ebene schwer nachvollziehen, welche Satzkonstruktionen möglich sind. Durch Modellierung von Fluent Interfaces in Form von Diagrammen wird versucht, diesen Nachteil zu meiden. Es wird dazu aus einem Modell der notwendige Mediator-Code automatisch generiert, sodass es nur noch nötig ist, das Verhalten des Fluent Interfaces selbst zu implementieren.
  • Eine lange Kette von Methodenaufrufen auf derselben Zeile erschwert deren Debugging, da ein Callstack typischerweise nur die Zeile des Fehlers enthält, nicht aber die Spalte im Source-File. Das Gleiche gilt für die Zuordnung von Warnungen aus der statischen Codeanalyse. Außerdem lassen sich Haltepunkte oft nur auf vollständige Anweisungen setzen, nicht auf einzelne Methodenaufrufe darin.

Einsatzmöglichkeiten

Fluent Interfaces werden für verschiedene Zwecke eingesetzt. Im Vordergrund steht immer, explizit zu machen, was in einer Domäne verankert ist.

  • Verpacken von Funktionalitäten
    Wie oben dargestellt, können Fluent Interfaces bestehende Funktionalitäten verständlicher anbieten.
  • Flüssiger Erbauer[5]
    Übertragung des Konzepts Fluent Interface auf das Entwurfsmuster Erbauer.
  • Abbildung fremder Syntax
    Mit Hilfe von Fluent Interfaces kann man im Programmcode auftretende Zeichenketten etwa für interpretierte Sprachen wie z. B. SQL, XPath oder HQL begrifflich leichter fasslich durch Aufrufe ersetzen.

Hinweis

Einige Programmiersprachen unterstützen benamte Parameter, z. B. Smalltalk oder ABAP. Bei diesen ist das Konzept der Fluent Interface nicht sinnvoll, da die Methodenschnittstellen bereits durch die Eigenschaften der verwendeten Sprache sprechend sein müssen.

Beispiel Smalltalk:

  object param1:foo param2:bar

Beispiel ABAP:

  lo_object->myMethod(
               iv_param1 = foo
               iv_param2 = bar
  ).

Weblinks

Einzelnachweise

  1. Martin Fowler: Fluent Interfaces. Bliki-Eintrag
  2. Specifications (PDF; 79 kB)
  3. Martin Fowler: Domain Specific Language. Bliki-Eintrag
  4. Evolving an Embedded Domain-Specific Language in Java. (PDF)
  5. a b Flüssiger Erbauer