Wie ich in meinem einleitenden Artikel zu diesem Blog geschrieben hatte, möchte ich den Entwicklungsweg, den das Flow-Design-Paradigma genommen hat, nachgehen. Deswegen soll es zunächst darum gehen, Event-Based Components (EBC), wie sie Ralf Westphal in seinem Artikel skizziert hat, in Scala zu realisieren.
Das EVA-Prinzip
Aus meiner Sicht adressiert EBC vor allem ein Grundproblem der Software-Entwicklung – Abhängigkeiten. Der EBC-Ansatz versucht den Entwickler in der Lösung dieses Problems zu unterstützen, indem Funktionalität in Funktionseinheiten (engl. Function Unit) gekapselt wird, die ganz dem EVA-Prinzip (Eingabe-Verarbeitung-Ausgabe) folgen. Eine Funktionseinheit hat einfließende Nachrichten, die sie verarbeitet. Das Ergebnis dieser Verarbeitung verlässt die Funktionseinheit wiederum als Nachricht.Wer die Nachricht sendet ist der empfangenden Funktionseinheit unbekannt. Es ist für die Verarbeitung in der Funktionseinheit auch nicht wichtig. Der Typ der eintreffenden Nachricht sowie der Typ der ausgehenden Nachricht muss natürlich der Funktionseinheit bekannt sein. Die Implementierung der Funktionseinheit ist somit von den Typen der einfließenden und ausfließenden Nachrichten abhängig. In einem Flow-Design-Diagramm werden deshalb die Typen ein- und ausfließender Nachrichten dargestellt, sowie bei Bedarf die Namen der Ein- und Ausgänge. Jedoch ist die Abhängigkeit von den zu verarbeitenden Datentypen eine natürliche Abhängigkeit und ihrem Wesen nach eine andere Abhängigkeit, als die von einer funktionalen Komponente und den Signaturen ihrer Methoden, wie bei objektorientierten Modellen üblich. Erst dadurch entsteht das Abhängigkeitsgeflecht typischer objektorientierter Implementierungen, und genau dieses wird im Flow-Design vermieden.
Nachrichten verarbeiten
Soweit eine kurze Einführung in das Grundkonzept der Funktionseinheiten des Flow-Designs. Aber wie könnte solch eine Funktionseinheit als Grundelement eines Programmierbaukastens in Scala realisiert werden? Anhand eines einfachen Beispiels soll die Umsetzung einer Funktionseinheit in Scala verdeutlicht werden. Diese Funktionseinheit empfängt eine Zeichenkette und wandelt sie in eine Zeichenkette um, die ausschließlich aus Großbuchstaben besteht. Die Funktionalität ist trivial, aber es geht hier mehr um das Prinzip. Statt einer einfachen Umwandlung wäre auch eine komplexere Verarbeitung denkbar.
Die Verarbeitung der über den „input“-Eingang eintreffenden Nachricht ließe sich naheliegenderweise folgendermaßen in Scala realisieren:
class ToUpper { def input(msg: String) { process(msg) } … }
Die eintreffende Nachricht wird in der Methode process verarbeitet. Diese Methode definiert die eigentliche Funktionalität der Funktionseinheit.
class ToUpper { … private def process(msg: String) { output(msg.toUpperCase()) } }
Nachrichten versenden
Das Ergebnis der Verarbeitung muss aus der Funktionseinheit heraus fließen. Dazu wird es einfach einer output-Methode übergeben, die den Ausgang output der Funktionseinheit symbolisiert und für die Ausleitung des Ergebnisses sorgt.
Diese output-Methode hat keine Implementierung in der Funktionseinheit ToUpper. Dazu müsste die Funktionseinheit ja „wissen“, wie das Ergebnis weiterverarbeitet wird. Dies ist während der Implementierung der Funktionseinheit unbekannt und gewährleistet gerade ihre implementationstechnische Unabhängigkeit. Erst wenn die Funktionseinheiten miteinander verbunden werden ist klar, wie output implementiert ist. Die Verbindung der Funktionseinheiten geschieht in einem späteren Schritt und ist von der Implementierung der Funktionseinheit völlig unabhängig. Deswegen muss es eine Möglichkeit geben, die Funktionalität für die Methode output zur Laufzeit, nach Instanziierung der Funktionseinheit, zu registrieren.
In der ursprünglichen EBC-Implementierung war diese output-Methode strukturell als Event-Sprachelement realisiert. Nun sind Events in C# nichts anderes als Listen von Funktionszeigern. Beim Auslösen eines Events werden alle für das Event registrierten Funktionen (C# Terminus: Event-Handler) nacheinander auf das dem Event-Aufruf übergebene Argument angewendet. Eine äquivalente Implementierung in Scala würde folgendermaßen aussehen:
class ToUpper { … private[this] var outputOperations: List[String => Unit] = List() private def output(msg: String) { outputOperations.foreach(operation => operation(msg)) } … }
In Scala sind Funktionen vollwertige Objekte, d.h. man kann sie Variablen zuweisen und Methoden als Argument übergeben. Um solche Funktionsvariablen und Funktionsparameter zu deklarieren, erlaubt Scala die Angabe von Funktionstypen. String => Unit ist ein Funktionstyp, der eine Funktionssignatur beschreibt, die einen String als Parameter definiert und nichts zurückliefert (also eine Methode ist). Demnach deklariert die Instanzvariable outputOperations eine Liste von Funktionsobjekten, die Methoden aufnehmen kann, die einen String als Argument verarbeiten.
In der Methode output() werden alle in outputOperations gespeicherten Methoden nacheinander auf den Parameter msg angewandt – genau wie die Event-Handler in C#. Jetzt fehlt nur noch die Registrierung der Funktionsobjekte in der Instanzvariablen outputOperations.
Wie oben erwähnt, können Funktionsobjekte auch als Parameter übergeben werden. Dies macht sich die Implementierung der Funktionseinheit zunutze und definiert eine Methode, der Methodenobjekte zur Registrierung als output-Operationen übergeben werden können.
class ToUpper { … def bindOutputTo(operation: String => Unit) { outputOperations = operation :: outputOperations } … }
Diese Methode wird aufgerufen, wenn die Funktionseinheiten miteinander verbunden werden. Die Verbindung der Funktionseinheiten erfolgt wiederum in speziellen Funktionseinheiten, die ausschließlich der Integration dienen und keine Funktionalität implementieren.
Registrierung zur Laufzeit
Eine mögliche Registrierung und damit die Anwendung der Methode bindOutputTo könnte z.B. folgendermaßen aussehen, wobei reverse die sendende Funktionseinheit ist, deren Ausgabe mit der Eingabe der toUpper-Funktionseinheit verbunden wird:
reverse.bindOutputTo(toUpper.input)
Da man in Scala den Punkt vor einer dereferenzierten Instanz-Methode weglassen kann, ebenso wie die Klammern bei einer einparametrigen Methode, lässt sich diese Registrierung auch wie folgt aufschreiben:
reverse bindOutputTo toUpper.input
Da in Scala Operatoren ganz normale Methoden sind, ließe sich bindOutputTo auch als „+=“-Operator definieren. Damit hätten wir eine C#-ähnliche Syntax:
reverse += toUpper.input
Aber das ist bei der Implementierung von Flow-Design-Funktionseinheiten in Scala gar nicht das Ziel. Scala ist eine mächtigere Sprache als C# und bietet die Möglichkeit, eine interne DSL für Flow-Design-Spezifikationen zu erstellen. Aber dazu und wie man Funktionseinheiten zusammensteckt im nächsten Artikel mehr…
Randnotiz
Der hier in Auszügen dargestellte Code kann auf GitHub nachgelesen werden.
Vielen Dank an Ronny Hapke für das Gegenlesen des Artikels! (rh)
Ich freu mich, EBC nun auch in Scala zu sehen. Da geht es natürlich einfacher als mit Java.
Auf die DSL bin ich gespannt. Bitte wieder eine kurze Notiz in der Google Group hinterlassen.
Und was ist mit anderen Übersetzung von Flow-Design? EBC sind ok, aber letztlich zu aufwändig, um damit alle Funktionseinheiten zu codieren. Wäre also auch schön zu sehen, wie es einfacher geht z.B. mit Continuations.
Hallo Ralf,
Das Ganze basiert schon auf Continuations — wird vielleicht klarer im nächsten Artikel, der schon in der Pipeline ist. Vielleicht verstehen wir beide aber auch Continuations unterschiedlich, warten wir die nächsten Artikel ab! Ansonsten ist die EBC-ähnliche Implementierung dazu gedacht, als Basis für die Implementierung einer Flow-Runtime in Scala zu dienen, neben dem Erlernen von Scala natürlich.
Die interne DSL kommt aber erst in einem späteren Artikel.
Gruß, Denis
Pingback: Extend Your Flow Horizon – Flow-Design mit Xtend | Beyond Coding