Named Parameters in Java mit Fluent Interfaces: Eine Annäherung

Gepostet am: 22. März 2016

Von

Durch Verwendung eines Fluent Interface kann man in Java einige Vorteile von Named Parameters nachbilden. In diesem Artikel zeigen wir, wie.

In einigen populären Sprachen gibt es das Feature der Named Parameters. Damit ist es möglich, beim Aufruf einer Methode oder Funktion über den Parameternamen festzulegen, welcher Parameter welchen Wert erhalten soll. Insbesondere ist die Reihenfolge der Parameter in der Methodendeklaration nicht relevant für die Zuordnung und es ist außerdem möglich, nicht alle vorhandenen Parameter mit Werten zu belegen. Im letzten Fall greifen dann Standardwerte oder Fehlerbehandlungen.

In Java ist dies bis jetzt so nicht möglich. Es muss immer für jeden Parameter ein Wert eines kompatiblen Typs übergeben werden und die Reihenfolge muss exakt eingehalten werden. Die Zuordnung erfolgt ausschließlich über die Reihenfolge, woraus folgt, dass an der Aufrufstelle gerade bei Methoden mit vielen Parametern und mehrmaligem Vorkommen derselben Typen nicht ohne weiteres erkennbar ist, welcher Parameter im Aufruf welchen Wert erhält.

Das Konzept des Method-Chaining

Nun bieten Fluent Interfaces das Konzept des Method-Chaining. Damit hat man die Option, den Methodenaufruf durch ein Objekt darzustellen, dem man in einer verketteten Abfolge von einparametrischen und ausdrucksstark benannten Methoden die einzelnen Parameterwerte für die letztliche Ausführung übergibt. Die Reihenfolge ist dabei irrelevant, die Mutatoren zeigen mit ihrem jeweiligen Namen eindeutig den gesetzten Parameter an und es müssen nur die Werte gesetzt werden, die man konkret belegen will. Alle weiteren werden mit Standardwerten belegt und fehlende Parameter können bei der Ausführung der Methode erkannt werden. Im Grunde handelt es sich um ein Parameter-Objekt-Pattern – und da hier immer dasselbe Objekt zurückgeliefert wird, haben wir es mit einem Fluent Interface ohne Grammatik zu tun.

Nehmen wir einen Service an, der eine Methode mit mehreren Parametern anbietet, wobei ich hier aus Platzgründen nur eine geringe Anzahl verwende.

Ein Aufruf daran sähe beispielsweise so aus:

Wenn man nun die Parameter, die die Aktion definieren in ein Parameter-Objekt mit einem Fluent Interface umwandelt und dieses im Service verwendet, sähe das so aus:

Es wären dann die folgenden Aufrufe möglich.

Das Parameter-Objekt kann natürlich auch über ein Builder Pattern mit Fluent Interface erzeugt werden, was recht praktisch ist wenn man eine Reihe von ähnlichen Aufrufen mit nur geringen Unterschieden in einzelnen Attributen ausführen möchte.

Der Methodenaufruf als Objekt

Es ist außerdem möglich, dass das Objekt nicht einfach die Parameter kapselt, sondern den ganzen Methodenaufruf repräsentiert. Der Aufrufer würde sich dabei mit Hilfe des Service eine Instanz dieser Action erzeugen, diese durch Belegen der Parameter mit Werten in einen validen Zustand bringen und letztlich an dieser Instanz beispielsweise eine action.invoke() Methode aufrufen.
Je nach Einsatzgebiet und Anwendungsfall kann diese Methode bereits die gesamte Funktion implementieren. Notwendige Kollaborateure kann der Service der Action Instanz bei Erzeugung übergeben haben, wodurch er für diese Funktion zu einer Factory wird. Alternativ kann der Service auch sich selbst an die Action übergeben, ggf. unter einem anderen Interface als dieses für den Aufrufer sichtbar ist, oder die Action kann als Innere Klasse implizit auf interne Funktionen des Service zugreifen. Hier ist hauptsächlich maßgebend, wie komplex die jeweilige Funktion ist und ob Kollaborateure aus einem DI-Kontext o.ä. involviert sind.

Die Action Instanz führt bei diesem Konzept einen Zustand mit, der abhängig von der Belegung der Pflichtparameter valide oder invalide ist. Nur in einem validen Zustand kann die Funktion erfolgreich ausgeführt werden. Allerdings ist das nur zur Laufzeit feststellbar. Es wäre doch wünschenswert, wenn man bereits zur Entwicklungszeit erkennen könnte, ob die Funktion valide ist oder nicht.

Das Fluent Interface ermöglicht es, bei jedem Methodenaufruf eine Instanz einer anderen Klasse zurück zu liefern. Es hat damit eine Grammatik und die beteiligten Klassen, die gemeinsam das Fluent Interface bilden, werden als Mediatoren bezeichnet. Wenn die Mediatoren die einzelnen Zustände eines entsprechenden Automaten repräsentieren, dann sind die Methoden die Transitionen zwischen diesen Zuständen. Zu Beginn hätte man also eine Instanz von InvalidAction und der Mutator für den letzten fehlenden Pflichtparameter würde dann eine Instanz von Action mit allen bisher gesammelten Werten liefern, die dann auch erst eine action.invoke() Methode aufweist.

Beim Aufruf sieht man von diesen Klassen erst einmal nichts, solange man die Methodenfolge nicht unterbricht.

Leider hat diese Vorgehensweise sehr starke Einschränkungen und weitere Nachteile.

Einschränkungen und Nachteile des Fluent Interface mit Grammatik

Der große Vorteil, dass nur Instanzen von validen Mediatoren eine .invoke() Methode haben, die also ansonsten gar nicht aufgerufen werden kann, und daher auch kein Validitätsprüfung zur Laufzeit durchführen muss, bringt mit sich, dass die Schnittstelle der Mediatoren sehr unflexibel wird und der innere Zustand der einzelnen Instanzen nicht zur Validierung beitragen kann. Effektiv bedeutet das, dass eine Transition immer nur zu einem Folgezustand führen kann, unabhängig davon, welcher Parameterwert der entsprechenden Methode eingegeben wird.

In diesem Beispiel muss ein invalidAction.withNumber(xx) immer zu einer validen Action führen. Sollte es fachliche Anforderungen an den Wert geben, z.B. nur positive Werte zu akzeptieren, ist diese Fallunterscheidung schlicht nicht möglich denn der Rückgabetyp der Methode ist als valide Action vorgegeben. Viel schwerwiegender als dieses Szenario ist die Tatsache, dass sämtliche Pflichtparameter mit Objekt-Typen diesen Ansatz unmöglich machen, weil nicht verhindert werden kann, dass die Mutatoren mit Null-Werten aufgerufen werden.

Klassenbasierte Validierung hat ihren Preis

Wie Eingangs erwähnt zeigt sich der praktische Nutzen des Konzepts erst wirklich, wenn man es mit einer größeren Anzahl von Parametern zu tun hat. Soll die Möglichkeit der beliebigen Reihenfolge bei der Parametereingabe aufrecht erhalten werden, wächst der Zustandsraum des Automaten sehr schnell, da für jede Permutation aller Pflichtparameter ein eigener Mediator angelegt werden muss. Die Mutatoren aller optionalen Parameter müssen dann an all diesen Klassen dupliziert werden. Eine Vererbung dieser gemeinsamen Methoden ist zwar möglich aber im Kontext eines Fluent Interface mit zusätzlicher Komplexität verbunden, wie ich in einem weiteren Artikel genauer ausführen werde.
Die Menge der anzulegenden Mediatoren kann auf eine lineare Entwicklung mit der Parameteranzahl reduziert werden, wenn man die Reihenfolge der Parametereingabe auf einen konkreten Pfad durch den Zustandsgraphen beschränkt. Dadurch geht aber wieder ein Teil der ursprünglich angestrebten Flexibilität verloren. Eine klassenbasierte Validierung ist also nur in einigen Fällen möglich und auch dann sollten Kosten und Nutzen gut abgewogen werden.

Solange die Validierung zur Laufzeit ausreicht, bietet ein Fluent Interface jedoch wie gezeigt eine gute Möglichkeit, sich dem Konzept der Named Parameters in Java anzunähern, wenn man darauf nicht verzichten möchte.

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.

2017-11-29T09:59:01+00:00