Railway Oriented Programming in Java

ROP

Anzeige

Die Idee, den Ablauf eines Programms analog zu Bahnschienen in einen Erfolgs- und einen Fehlerweg einzuteilen, stammt von Scott Wlaschin, der sie bei der funktionalen Softwareentwicklung mit F# einsetzt. In diesem Video zeigt er ausführlich den Praxiseinsatz des Railway Oriented Programming:

Das Konzept hinter ROP lässt sich aber ohne Weiteres auch auf andere Programmiersprachen übertragen. Und dank der Lambdas in Java 8 ist nun auch eine einfache Implementierung in Java möglich. Das Problem vieler Methoden wie der im zweiten Beispiel gezeigten ist, dass der normale Programmfluss nur durchlaufen wird, wenn bei keinem Zwischenschritt ein Fehler auftritt. Sobald man jedoch an irgendeiner Stelle vom erwarteten Verhalten abweicht (z. B. weil das alte Passwort nicht gültig oder ein Datenbankfehler aufgetreten ist), soll fast immer der restliche Code nicht mehr durchlaufen und stattdessen entweder eine Exception geworfen oder ein Standardwert wie "null" zurückgegeben werden. Der Fehlerfall führt das Programm also wie bei einer Weiche auf eine andere Bahnschiene, die parallel zum normalen Programmfluss verläuft, aber eben komplett daran vorbeiführt und sich bis zum Ende der Befehlskette nicht wieder verlassen lässt (s. Abb. 1).

Wie bei einer Weiche für Bahnschienen wird im Fehlerfall der normale Weg durch den Code verlassen (Abb. 1)

Damit das funktioniert, wird ein Objekt benötigt, das den aktuellen Zustand der Operationskette repräsentiert. Da es um deren Ausgang geht, könnte man es auf Deutsch Ergebnis nennen oder auf Englisch Result. Das Objekt führt nun abhängig vom aktuellen Zustand die übergebene Operation aus (und wechselt dabei gegebenenfalls den Zustand) oder eben nicht. Um die Operationen abhängig vom aktuellen Zustand ausführen zu können, sind sie in einer Form an das Objekt zu übergeben, die es ihm ermöglichen, die Ausführung zu kontrollieren. Hierzu bieten sich Lambda-Ausdrücke an.

Das folgende Beispiel zeigt eine erste Implementierung der Klasse Result in Java:

public final class Result {
private final String value;
private final String error;

private Result(final String value, final String error) {
this.value = value;
this.error = error;
}

public static Result withValue(final String value) {
return new Result(value, null);
}

public static Result withError(final String error) {
return new Result(null, error);
}

public boolean isFailure() {
return error != null;
}

public boolean isSuccess() {
return !isFailure();
}

public String getValue() {
return value;
}

public String getError() {
return error;
}

public Result onSuccess(final Consumer<String> consumer) {
if (isSuccess()) {
consumer.accept(getValue());
}
return this;
}

public Result onFailure(final Consumer<String> consumer) {
if (isFailure()) {
consumer.accept(getError());
}
return this;
}
}

Sie ist ein simpler Wrapper um zwei String-Attribute: value enthält den Wert des Ergebnisses, falls es erfolgreich ist, und error die Fehlermeldung, falls es fehlerhaft ist. Einige Hilfsmethoden erlauben die Abfrage des aktuellen Zustands (isFailure() und isSuccess()) beziehungsweise ermöglichen den Zugriff auf Wert und Fehler des Ergebnisses (getValue() und getError()). Erzeugt wird das Objekt über die Factory-Methoden withValue() beziehungsweise withError(), damit sich nicht gleichzeitig ein Wert und ein Fehler setzen lassen.

Die Methoden onSuccess() und onFailure() zeigen nun die eigentliche Aufgabe von Result. Sie führen die übergebene Operation (java.util.function.Consumer<String>: eine Funktion, die einen String als Parameter bekommt und keinen Rückgabewert hat) nur aus, falls sich das Objekt im jeweiligen Zustand befindet. Der Operation wird der Wert des Ergebnisses beziehungsweise des Fehlers übergeben. Als Rückgabewert dient immer das Objekt selbst, damit sich die Aufrufe der Methoden aneinanderreihen lassen, wie im folgenden Beispiel zu sehen ist.

System.out.println("=== Success ===");
Result.withValue("My value")
.onSuccess(value -> System.out.println("The value: " + value))
.onFailure(error -> System.out.println("The error: " + error));

System.out.println("\n=== Failure ===");
Result.withError("My error")
.onSuccess(value -> System.out.println("The value: " + value))
.onFailure(error -> System.out.println("The error: " + error));

Die resultierende Ausgabe ist:

=== Success ===
The value: My value

=== Failure ===
The error: My error
Anzeige