© Excellent backgrounds/Shutterstock.com
Teil 4: Flow Design mit Scala

Treppengeplätscher


Der Flow-Design-Ansatz ist so fundamental wie der objektorientierte oder der funktionale, sodass sich die Umsetzung als eigenständiges Sprachkonstrukt geradezu aufdrängt. Doch es muss nicht gleich eine eigene Programmiersprache sein. In Scala lassen sich über interne DSLs Spracherweiterungen hinzufügen.

Das Grundprinzip des Flow-Design-Ansatzes ist die gegenseitige Nichtbeachtung [1] der beteiligten Funktionseinheiten einer Abstraktionsebene. Sie sollen sich nicht kennen, um so konsequent Abhängigkeiten zu vermeiden. Nur die Funktionseinheit, die diese über ihre Ports miteinander verbindet, hat Abhängigkeiten zu den integrierten Funktionseinheiten. Enthält eine solche integrierende Funktionseinheit keine weitere Logik außer die integrierten Funktionseinheiten zu verbinden, implementiert sie das Grundprinzip einer IODA-Architektur, in der Integration und Operation strikt voneinander getrennt werden [2].

kuniss_scalaflow_1.tif_fmt1.jpgAbb. 1: Schema einer Funktionseinheit im Flow Design

Eine Funktionseinheit im Flow Design folgt dem Muster wie in Abbildung 1 gezeigt. Nachrichten erreichen eine Funktionseinheit über einen Port, Ergebnisse verlassen eine Funktionseinheit über einen Port. Dies entspricht dem EVA-Prinzip, dass wir auch aus der funktionalen Programmierung kennen und das Wikipedia als das Grundprinzip der Datenverarbeitung beschreibt [3]. Eine Funktionseinheit ist damit nur von den Typen der einfließenden und ausfließenden Nachrichten abhängig – eine natürliche Abhängigkeit und auch eine unvermeidbare.

Eine Scalaflow DSL

Die Scalaflow DSL ist in mehreren Schritten während der Auseinandersetzung mit der Sprache Scala entstanden. Es ist eine mögliche Realisierung einer Flow Design DSL. Die DSL verwendet das Scala-Sprachkonstrukt der Traits, um Ports zu realisieren. Des Weiteren werden spezielle Operatoren definiert, um das Verbinden der Ports in integrierenden Funktionseinheiten zu spezifizieren und die Ausleitung der Berechnungsergebnisse in Ausgabeports anzugeben. Aber wie sieht dies im Detail aus? Eine Funktionseinheit wird als Klasse von der Basisklasse FunctionUnit abgeleitet. Ports werden als generische Traits InputPort und OutputPort deklariert, mit denen diese Klasse dekoriert wird. So würden z. B. die beiden Ports einer Funktionseinheit ToUpper wie folgt deklariert werden (Abb. 2):

class ToUpper extends FunctionUnit("ToUpper") with InputPort[String] with OutputPort[String] { val in = InputPort val out = OutputPort("out") ... }
kuniss_scalaflow_2.tif_fmt1.jpgAbb. 2: Beispiel einer simplen Funktionseinheit

Traits werden in Scala mit dem Schlüsselwort with dort eingeleitet, wo in Java Interfaces angegeben werden. Sie werden mit dem Datentyp des Ports parametrisiert, hier String. Durch die Dekoration der Klasse mit den beiden Traits werden automatisch die vordefinierten Ports input und output als Felder der Klasse verfügbar. Um sprechende Portnamen zu erhalten, können auch Felder mit selbst gewählten Namen für Ports definiert werden, wie für in und out geschehen. Die beiden Traits InputPort und OutputPort sind für Funktionseinheiten gedacht, die nur einen Eingang oder einen Ausgang haben. Sie definieren zusätzliche spezielle Operatoren, die es erlauben, beim Verbinden nur die Funktionseinheiten anzugeben, ohne die Ports explizit benennen zu müssen. Für den häufigen Fall, dass eine Funktionseinheit nur einen Eingangs- oder Ausgangsport hat, ermöglicht dies eine kürzere Schreibweise, wie wir später sehen werden. Hat eine Funktionseinheit jedoch mehr als einen Eingangs- oder Ausgangsport, gibt es dafür die Traits InportPort1, OutputPort1, InputPort2, OutputPort2 usw. Sie fügen der Funktionseinheit jeweils Ports mit den Namen input1, output1, input2, output2 usw. hinzu.

Eingangsdaten verarbeiten

Daten, die eine Funktionseinheit über ihren Eingangsport erreichen, werden über die durch den Trait InputPort definierte abstrakte Methode processInput verarbeitet (respektive processInput1, processInput2 usw.). Diese Methode muss spezifisch implementiert werden und enthält die Logik der Funktionseinheit. Sobald der jeweilige Trait der Klasse hinzugefügt wurde, wird eine Implementierung dieser abstrakten Methode vom Scala-Compiler eingefordert und bei Verwendung von Scala-IDEs auch durch Quick-Fix-Vorschläge unterstützt.

class ToUpper { protected def processInput(msg: String) {  ... } }

Berechnungsergebnisse weiterleiten

Die durch die Funktionseinheit in den processInput-Methoden berechneten Ergebnisse können über den Operator <= an einen der deklarierten Ausgabeports weitergeleitet werden, damit die nächste Funktionseinheit, die mit diesem Ausgabeport verbunden ist, die Ergebnisse weiterverarbeiten kann. Hier ein Beispiel:

output <= msg.toUpperCase()

Der Weiterleitungsoperator akzeptiert auch Closures, ein Sprachkonstrukt, das mit Version 8 nun endlich auch in Java angekommen ist.

output <= { if (msg.startsWith("_")) msg.toFirstUpper else msg.toUpperCase }

In Scala ist, wie in vielen anderen funktionalen Sprachen, das Ergebnis des letzten Ausdrucks eines Statementblocks immer auch dessen Rückgabewert. Dieser wird damit auch als Rückgabewert der Closure dem Ausgabeport zugewiesen.

Wie im zweiten Artikel [2] beschrieben, implementiert Flow Design automatisch eine IODA-Architektur, der zufolge Operationen und Integrationen getrennt werden sollen. Deswegen gibt es auch in Scalaflow integrierende und operative Funktionseinheiten. Die Deklaration der Schnittstelle unterscheidet sich bei be...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang