Im letzten Artikel hatte ich an einem Beispiel gezeigt, wie eine EBC-Funktionseinheit in Scala aussehen könnte. Nun soll es darum gehen, wie mehrere solcher Funktionseinheiten zusammengesteckt werden können.
Dazu soll folgendes Flow-Design-Modell als Beispiel Verwendung finden:
Die Semantik des Modells ist extra simpel gehalten: Eine Zeichenkette fließt in die Funktionseinheit Reverse hinein, die die Zeichen in ihrer Reihenfolge umdreht. Die umgedrehte Zeichenkette fließt danach jeweils in die beiden Funktionseinheiten ToUpper und ToLower. Wie die Namen der beiden Einheiten nahelegen, wandelt die eine alle Kleinbuchstaben in Großbuchstaben um, während die andere genau das Umgekehrte vollbringt. Die beiden veränderten Zeichenketten werden dann von der Funktionseinheit Collector empfangen und durch Verkettung wieder zu einer Zeichenkette vereinigt. Diese fließt danach als Ergebnis aus dem Collector heraus.
Flow DSL
Im Flow-Design verwenden Ralf Westphal und Stefan Lieser folgende kleine DSL, um Flow-Modelle, ohne ein Diagramm zu zeichnen, formal auszudrücken:
/ .in, Reverse Reverse, ToLower Reverse, ToUpper ToLower, Collector.input1 ToUpper, Collector.input2 Collector, .out
Das Zeichen „/“ ist dabei die zentrale Funktionseinheit, in der alles zusammengesteckt wird; sozusagen die „Main“-Funktionseinheit. Ein Name mit einem Punkt nach einem Funktionseinheitsnamen spezifiziert einen Ein- oder Ausgang eindeutig, falls es mehr als einen gibt. Wenn es nur einen Eingang oder Ausgang gibt, werden deren Namen einfach weggelassen. Ein Name mit einem Punkt davor, ohne Funktionseinheit, symbolisiert einen Ein- bzw. Ausgang der umgebenden Funktionseinheit. Verbunden ist immer ein Ausgang links vom Komma mit einem Eingang rechts davon – es sind immer Paare. So lässt sich ein Fluss auf derselben Abstraktionsebene eindeutig beschreiben.
Ralf und Stefan haben für diese DSLs auch Werkzeuge implementiert, die automatisch grafische Repräsentationen erzeugen oder diese interpretieren. Diese Werkzeuge sind jedoch stark .NET-lastig und lassen sich eher nicht so leicht in Eclipse integrieren. Aber zum Glück ist das Eclipse-Universum mächtig genug, um hier mit relativ geringem Aufwand Ersatz zu schaffen. Aber soweit sind wir noch nicht, zuerst einmal der nächste Schritt auf dem Flow-Design-Weg.
Warter und Sammler
Vor diesem möchte ich jedoch noch mal kurz auf die Funktionseinheit Collector eingehen. Sie hebt sich in zweierlei Hinsicht von den anderen im Diagramm gezeigten Funktionseinheiten ab. Einerseits hat sie zwei Eingänge. Anderseits müssen auf beiden Eingängen Nachrichten vorliegen, denn die Funktionalität der Funktionseinheit ist es, den Inhalt der beiden Nachrichten zu verknüpfen.
Die Eingänge der Funktionseinheit sind ähnlich den anderen Funktionseinheiten, nur dass es eben zwei sind und sie deswegen namentlich unterschieden werden müssen:
class Collector { def input1(msg: String) { accumulateInput(msg) } def input2(msg: String) { accumulateInput(msg) } ... }
Die Methode accumulateInput implementiert die Semantik dieser Funktionseinheit:
class Collector(val separator: String) { ... private[this] var accumulation: List[String] = List() private def accumulateInput(msg: String) { accumulation = msg :: accumulation if (accumulation.length == 2) output(accumulation mkString(separator)) } }
Die Methode speichert die eingehenden Zeichenketten in der internen Liste accumulation. Listen funktionieren in Scala so ähnlich wie in Lisp. Durch den zweifachen Doppelpunkt wird ein neues Element an den Kopf der Liste angefügt. Die zuletzt eintreffende Zeichenkette steht im Ergebnis also vorne. Das Ergebnis wird durch eine Listen-Hilfsfunktion gebildet – mkString. Diese liefert die Repräsentation der Liste als Zeichenkette zurück, wobei der Funktion eine Separator-Zeichenkette übergeben werden kann, die zwischen den Listenelementen eingefügt wird. Dieser Separator wird bei der Konstruktion der Funktionseinheit festgelegt, weswegen die Klassendeklaration eine Parameterliste hat. Dies ist die Form, in der in Scala primäre Konstruktoren angegeben werden.
class Collector(val separator: String)
Sobald die Liste zwei Elemente hat, wird das Ergebnis über output ausgeliefert. Diese Ausgabefunktion ist ganz genauso implementiert wie in der ToUpper-Funktionseinheit.
Es sei noch angemerkt, dass diese Funktionseinheit nur zum einmaligen Gebrauch ist. Würde nämlich eine dritte Zeichenkette eintreffen, dann würde sie den beiden vorhanden Zeichenkette in der Liste vorangestellt und alles zusammen sofort als Ergebnis geliefert; es gibt keine Rücksetzmöglichkeit für die Akkumulationsvariable. Aber für die hier vorgestellte Realisierung ist dies völlig ausreichend – die Funktionseinheit wird nur einmal benutzt. Mir ist dies zwar erst aufgefallen, während ich das hier aufschreibe, aber trotzdem würde ich es nicht ändern. Es macht den exemplarischen Code einfacher und schlussendlich gilt auch hier das KISS-Prinzip.
Zusammenstecken
Wie nun die vielen Ein- und Ausgänge verbinden und das Ganze ausführen?
In Scala gibt es wie in C# oder Java eine Methode main als zentralen Modul-Eintrittspunkt. Das Scala Idiom für die Main-Implementierung lautet:
object RunFlow { def main(args: Array[String]) { ... } }
Dabei kann man sich das Konstrukt mit der Schlüsselworteinleitung object als Klassendeklaration und Objekt-Instanziierung in einem denken. In Scala gibt es keine statischen Klassenelemente mehr. Diese werden durch das designtechnisch sehr viel sauberere Sprachkonzept object ersetzt, das auch noch andere Vorteile bringt.
In dem vorgestellten Beispiel erfolgt das Verbinden der Ein- und Ausgänge der einzelnen Funktionseinheiten in der main-Methode. Dazu müssen die Funktionseinheiten zunächst instanziiert werden.
object RunFlow { def main(args: Array[String]) { println("instantiate flow units...") val reverse = new Reverse val toLower = new ToLower val toUpper = new ToUpper val collector = new Collector(", ") ... } }
Nach der Instanziierung werden entsprechend dem Modell die Ausgänge mit den zugehörigen Eingängen verbunden. Jede Pfeilverbindung im Flow-Design-Modell wird durch ein Paar Funktionseinheiten repräsentiert, die mit Hilfe der Methode bindOutputTo verknüpft werden.
object RunFlow { def main(args: Array[String]) { ... println("bind them...") reverse bindOutputTo toLower.input reverse bindOutputTo toUpper.input toLower bindOutputTo collector.input1 toUpper bindOutputTo collector.input2 collector bindOutputTo(msg => { println("received '" + msg + "' from " + collector) }) ... } }
Die Ausgabe der Funktionseinheit Collector wird am Ende auf der Konsole ausgegeben. Dazu wird ein Funktionsliteral definiert, das als dynamisch erzeugtes Funktionsobjekt der Methode bindOutputTo der Instanz collector übergeben wird.
Jetzt verbleibt nur noch, den Flow zu starten.
object RunFlow { def main(args: Array[String]) { ... println("run them...") val palindrom = "Trug Tim eine so helle Hose nie mit Gurt?" println("send message: " + palindrom) reverse.input(palindrom) println("finished.") } }
Ergebnis der Ausführung ist folgende Konsolen-Ausgabe:
instantiate flow units... bind them... run them... send message: Trug Tim eine so helle Hose nie mit Gurt? received '?trug tim ein esoh elleh os enie mit gurt, ?TRUG TIM EIN ESOH ELLEH OS ENIE MIT GURT' from Collector finished.
Zusammenfassung
Flow-Design funktioniert auch in Scala. Das stand auch nicht in Frage – es ist ein allgemeines Konzept. Aus meiner Sicht sogar ein neues Programmierparadigma. Schon deshalb wert, es auch in der Java-Welt verfügbar zu haben.
Es lässt sich mit den Mitteln von Scala recht geschickt umsetzen. Aber es geht mit Scala noch besser, besser als in C#. Davon soll der nächste Artikel handeln.
Randnotiz
Der hier gezeigte Code ist bei Github verfügbar.
Falls der eine oder andere sich fragt, warum das Scala-Paket als actorlessflow benannt ist: Ich hatte den ersten Versuch, Flow-Design zu implementieren, mit der Scala-Actor-Bibliothek gestartet. Aber es hatte sich dann in einem zweiten, dem hier beschriebenen, Implementierungsentwurf recht schnell herauskristallisiert, dass dieses Konzept für einfache, synchrone Flow-Implementierung nicht notwendig ist, das Ganze semantisch nur überfrachtet wird.
Vielen Dank an Ronny Hapke für das Gegenlesen des Artikels. (rh)
Pingback: Ein spezieller Charakterzug – Traits für Flow-Design in Scala | Beyond Coding