Flow Designs zu implementieren, ist nun einen ganzen Schritt einfacher geworden. Endlich habe ich nämlich Zeit gefunden, die Flow Runtime mit etwas mehr "Intelligenz" zu versehen.
Bisher bestand die Implementation eines Flow Designs aus drei Teilen:
- Flow
- Operationen
- Bindung
Als Beispiel für einen Flow hier der für die Umformatierung einer Datei in ganz einfacher Weise. Ihre Zeilen sollen auf eine neue maximale Länge gebracht werden:
/
.run, Zeilen_lesen
Zeilen_lesen, Umbrechen
Umbrechen, Zeilen_schreiben
Die Operationen sind Methoden, die wohl ganz angemessen auf zwei Klassen verteilt werden können:
class Textdatei {
private string _dateiname;
public string[] Zeilen_lesen(string dateiname) {…}
public void Zeilen_schreiben(string[] zeilen) {…}
}
class Formatierung {
public static string[] Umbrechen(string[] zeilen) {…}
}
Gebunden werden die Operationen an den Flow über die Konfiguration der Flow Runtime:
var td = new Textdatei();
var config = new FlowRuntimeConfiguration()
.AddStreamsFrom("umbrechen.root.flow", Assembly.GetExecutingAssembly())
.AddFunc<string, string[]>("Zeilen_lesen", td.Zeilen_lesen)
.AddAction<string[]>("Zeilen_schreiben", td.Zeilen_schreiben)
.AddFunc<string[], string[]>("Umbrechen", Formatierung.Umbrechen);
using(var fr = new FlowRuntime(config)) {
fr.Process(".run", args[0]);
fr.WaitForResult();
}
Das funktioniert wunderbar. Doch es ist bei der Konfiguration umständlich. Die stellt nämlich auch einen Widerspruch zu DRY dar. In ihr werden ja Dinge wiederholt, die anderswo schon stehen. Vor allem ist es aber mühsam, in der Konfiguration die Signaturen der Operationsmethoden anzugeben. Das macht die dynamische Natur der kleinen Flow DSL quasi nutzlos.
Damit ist nun Schluss!
Jetzt ist die Konfiguration viel, viel einfacher. Sie schrumpft im Grunde auf zwei Zeilen Code, die sich im Verlauf der Evolution einer Anwendung kaum mehr ändern müssen:
var config = new FlowRuntimeConfiguration()
.AddStreamsFrom("umbrechen.root.flow", Assembly.GetExecutingAssembly())
.AddOperations(new AssemblyCrawler(Assembly.GetExecutingAssembly()));
Eine Zeile liest den Flow, eine weitere Zeile lädt alle Operationen.
Der AssemblyCrawler durchläuft alle Typen der ihm übergebenen Assemblies und schaut, ob die Operationsmethoden definieren. Wenn ja, registriert er sie automatisch. Berücksichtigt werden Klassen, die mit einem von drei Attributen markiert sind:
[InstanceOperations]
class Textdatei { … }
[StaticOperations]
class Formatierung { … }
Klassen, die mit [StaticOperations] gekennzeichnet sind, werden auf statische Methoden überprüft, Klassen, die mit [InstanceOperations] markiert sind, werden zuerst instanziert, um dann auf Instanzmethoden überprüft zu werden. Und schließlich gibt es noch [EventBasedComponent], um Instanzen zu registrieren, die Output über Events hinausgeben.
Relevant als Operationen sind in jedem Fall nur Methoden, die den Signaturen entsprechen, die auch über AddAction<>() und AddFunc<>() registriert werden können. Einen Unterschied gibt es dabei jedoch: Output-Ports, die über Continuations definiert sind, haben nicht mehr die Namen ".out0" und ".out1", sondern die der Continuation-Parameter.
Der Crawler macht die Registrierung aller Operationen in den übergebenen Assemblies sehr einfach. Wer jedoch etwas mehr Kontrolle haben möchte, kann mit Attributen markierte Klassen auch von Hand registrieren:
var config = new FlowRuntimeConfiguration()
.AddStreamsFrom("umbrechen.root.flow", Assembly.GetExecutingAssembly())
.AddInstanceOperations(new Textdatei())
.AddStaticOperations(typeof(Formatierung));
Ich habe jetzt einige Tage mit dieser Form der einfachen Konfiguration gearbeitet… und möchte sie nicht mehr missen. Jetzt endlich kann ich mich wirklich auf Flow und Operationen konzentrieren. Jetzt endlich löst die Flow Runtime ihr Versprechen ein, simpler und gleichzeitig leistungsfähiger als EBC-Verdrahtung zu sein.
Hier geht's zum Download der aktuellen Flow Runtime NPantaRhei.