Follow my new blog

Montag, 22. März 2010

TDD, aber bitte mit System

Hier sind zwei recht ordentliche Artikel zum Thema TDD: Teil 1 und Teil 2. Mir gefällt daran besonders, dass Neal Ford sein TDD beginnt mit einer kleinen Liste von Schritten auf dem Weg zur Beantwortung der Frage, ob eine Zahl eine Vollkommene Zahl ist.

Bei allem Gefallen an seinen Artikeln reibe ich mich jedoch an einigen Punkten.

Fragliche Objektorientierung

Wenn Sie die Wurzel einer Zahl berechnen wollten und ich würde Ihnen sagen, das geht mit C# so:

var sqc = new SqrtCalculator(7);
Console.WriteLine(“Wurzel aus 7: {0}”, sqc.Calculate());

Würden Sie das als naheliegend empfinden? Kaum.

Oder wenn ich Ihnen anbieten würde, einen String bei einer Zeichenkette so zu splitten:

var splitter = new StringSplitter(“ ”);
var words = splitter.Split(“the quick brown fox”);

Würden sie das als komfortable ansehen? Kaum.

Nichts anderes schlägt Neal Ford aber vor, wenn er seine Implementation zur Prüfung von Zahlen auf Vollkommenheit so aussehen lässt:

var c = new Classifier(6);
Console.WriteLine(“Ist 6 vollkommen: {0}”, c.IsPerfect());

Das ist genausowenig komfortabel oder naheliegend wie die obigen Beispiele. Das ist erzwungen objektorientiert. Das ist ein für den Anwender der Funktionalität nicht nachvollziehbarer API.

Die Prüfung, ob eine Zahl vollkommen ist, ist eine Funktion genauso wie die Prüfung, ob eine Zahl gerade ist oder die Berechnung ihrer Wurzel. Warum also sollte ich als Nutzer einer solchen Funktion(alität) eine Klasse instanzieren müssen? Warum sollte diese Klasse auch noch zustandsbehaftet sein?

Neil leitet das auf Seite 8 seines Teil 1 daraus ab, dass zwei interne Funktionen (factorsFor(int number) und isFactor(int number)) auf demselben Parameter arbeiten, der zu prüfenden Zahl. Er meint, das sei nun wirklich zuviel prozedurale Programmierung. Oder vielleicht scheut er sich vor der “Feuchtigkeit” der Wiederholung eines Parameters? Ich weiß es nicht. Ich finde es einfach nur überkandidelt – und zwar leider in einer Weise die symptomatisch ist für die Branche. Denn sein Vorgehen ist ja tragisch: Er will das Gute erreichen, verschlimmert die Situation aber. Er möchte einem Paradigma dienen, das für sich gepachtet zu haben scheint, dass es durch und durch gut sei – aber das Paradigma passt leider nicht zum Problem. Mit dem Hammer Objektorientierung wird die funktionale Schraube eingeschlagen. Autsch!

Hier dagegen meine Lösung:

public class VollkommeneZahlen

{

    public static bool IstVollkommen(int zahl)

    {

        return EchteTeilerVon(zahl)

              .ErgebenEineVollkommeneZahl(zahl);

    }

    …

Die Nutzung sieht so aus:

Console.WriteLine(VollkommeneZahlen.IstVollkommen(6));

Das halte ich sowohl in der Anwendung wie auch in der Implementation für intuitiv und angemessen. Egal, ob das nun speziell objektorientiert oder sonstwas ist. Der Anwender versteht es, weil eine Funktion als Funktion realisiert ist. Und der, der den Code liest, sieht sofort, wie das Ergebnis in zwei Schritten zustande kommt.

Unklare Verteilung von Verantwortung

Nicht nur ist aber die Objektorientierung für das Problem unangemessen, wie ich finde. Sie hat sogar einen negativen Effekt auf die Verständlichkeit und Flexibilität des Codes.

Vollkommene Zahlen sind solche, deren echte Teiler in Summe die Zahl ergeben. Die Teiler von 6 sind 1, 2, 3 und 6. Allerdings ist 6 kein echter Teiler. Die Summe ist deshalb nur aus 1, 2 und 3 zu bilden und die ergibt 6. Damit ist 6 eine Vollkommene Zahl.

In Neal Fords Code ist die besondere Behandlung der Zahl selbst als unechter Teiler an zwei Stellen codiert:

image

Er fügt die Zahl selbst als Teiler zunächst der Menge aller Teiler hinzu – und zieht sie am Ende wieder ab. Damit erfüllt auch seine Funktion calculateFactors() nicht mehr wirklich ihre Verantwortlichkeit, die Teiler der Zahl zu berechnen. Das trägt nicht zur Verständlichkeit der Algorithmusimplementierung bei.

Außerdem ist isPerfect() nicht nur von der Zahl als Zustand abhängig, sondern auch noch indirekt von der Menge der Teiler, die calculateFactors() berechnet und sumOfFactors() auswertet. Solcher Zustand erhöht immer die Komplexität einer Lösung. Und er macht die Wiederverwendung von Methoden in anderen Zusammenhängen schwieriger.

Meine Funktion IEnumerable<int> EchteTeilerVon(int zahl) hingegen ist von nichts abhängig und kann daher in jedem Zusammenhang, in dem nur die echten Teiler einer Zahl relevant sind, wiederverwendet werden.

Auch macht bool ErgebenEineVollkomeneZahl(this IEnumerable<int> teiler, int zahl) den Code selbstdokumentierender. Das Abstraktionsniveau der Bestandteile von IstVollkommen() ist einheitlich. Bei Neal hingegen liegen calculateFactors() und der Ausdruck zur Berechnung des Rückgabewertes auf unterschiedlichen Abstraktionsniveaus. Der Leser muss sich zusammenreimen, dass eine Zahl vollkommen ist, wenn die Summe von Teilern abzüglich einer Zahl gleich der Zahl ist. Zugegeben, das ist nicht so kompliziert, doch es ist ein spürbarer intellektueller Aufwand, der nicht Not tut. (Insbesondere verwunderlich ist sein Ansatz, da er in einem anderen Artikel das Single Level of Abstraction Prinzip lobt.)

Gesucht: Klares Vorgehen

Schließlich vermisse ich bei Neals Artikeln, dass er sein Vorgehen nicht weiter deutlich sichtbar systematisiert. Er tut schon das Richtige, in dem nicht mit TDD “reinspringt” und als erstes einen Test für isPerfect() schreibt, sondern mal einen Gedanken an den Algorithmus verschwendet. Er denkt also nach, bevor er codiert. Doch das hebt er nicht hervor. Einzig der “TDD workflow” wird als Handlauf für das Vorgehen wieder einmal thematisiert.

Schade. Denn zu TDD gehört mehr, denke ich. Hier mein Version eines Entwicklungsprozesses, in den TDD eingebettet sein sollte:

  1. Verstehen
  2. Nachdenken/planen
  3. Unsicherheiten ausräumen
  4. Codieren

Diese Schritte beziehen sich auf ein Problem bzw. auf die das Problem lösende Funktionseinheit. Sie sind daher rekursiv zu durchlaufen, sollte die Funktionseinheit in weitere zerfallen. Das ist ein wichtiger Punkt! Denn auf Zerlegungsebene eines Problem gilt es zuerst zu verstehen, dann zu planen usw.

Zu 1: Am Anfang der Entwicklung einer Softwarelösung – sei es Methode oder Klasse oder Komponente oder Anwendung – steht das Verstehen. Investieren Sie Zeit, das Problem oder gar den Kunden zu verstehen. Machen Sie sich klar, was die Anforderungen wirklich sind. Tragen Sie Fälle zusammen, die beschreiben, was die Eingaben/Parameter und der zugehörigen erwarteten Ausgaben/Ergebnisse sind. Das hört sich selbstverständlich an. In der Praxis ist aber schwierig und bedarf immer wieder Mut und Disziplin. Beide werden oft nicht ausgebracht. Schöner ist es, schnell den Code Colt zu ziehen und zu programmieren.

Leider ist ungenügendes Verständnis aber wohl eine der wesentlichen Ursachen für soviele Probleme der Softwareentwicklung. Wer ungenügend versteht, der implementiert zuviel. Wer ungenügend versteht, der implementiert das falsche oder inkorrekt.

Letztlich ist ungenügendes Verständnis zwar nie ganz zu vermeiden, doch ein wenig mehr Mühe darf es schon sein. Vor allem ist in den Prozess des Verständnisaufbaus der Kunde sehr aktiv mit einzubeziehen. Fragen Sie ihm Löcher in den Bauch! Dafür ist er da ;-) Lassen Sie ihn nicht aus seiner Verantwortung. Wenn er etwas von Ihnen will, dann soll er wirklich so genau wie möglich beschreiben, was das ist, wie es aussehen soll, wie es sich verhalten soll. Erbitten Sie von ihm sehr konkrete Abnahmetests.

Falls der Kunde sich bei solch “peinlichen Befragung” ziehrt, machen Sie ihm klar, dass das Ergebnis Ihrer Arbeit nur so gut sein kann wie seine Spezifikation. Garbage in, garbage out – das gilt auch hier. Wer nur ungenau spezifiziert, der kann auch nur ein ungenaues Ergebnis bekommen. Dann muss nachgebessert werden. Das macht niemandem Freude.

Da die meisten Kunden mit sehr genauer Spezifikation überfordert sein werden, gibt Ihnen das die Chance, ein agiles Vorgehensmodell “zu verkaufen”. Nutzen Sie die Chance! Aber nehmen Sie das nicht als Entschuldigung, ohne gründliches Verständnis mit dem Codieren zu beginnen.

Zu 2: Wenn Sie meinen, das Problem durchdrungen zu haben, sollten Sie noch nicht zur Tastatur greifen. Auch nicht, wenn Sie TDD betreiben wollen. Tun Sie sich den Gefallen und denken Sie zuerst über einen Lösungsansatz nach. Diese Phase fällt leider immer wieder zu kurz aus. Auch in wohlmeinenden Coding Dojos wird das nicht unbedingt praktiziert. TDD als Test-Driven Design verstanden soll es richten. Codieren kann Planung und Nachdenken aber nicht ersetzen, sondern allenfalls unterstützen. Eine Struktur für Ihre Software ergibt sich nicht einfach. Die will bewusst entworfen werden.

Das ist, was mir an Neals Artikel gefallen hat: Er hat über das Problem nachgedacht und einen kleinen Plan aufgestellt. Er ist auf zumindest drei Funktionseinheiten/Verantwortlichkeiten gekommen, aus denen eine Lösung für das Problem “Vollkommene Zahlen erkennen” besteht:

  • Potenzielle Teiler einer Zahl erzeugen
  • Feststellen, ob eine potenzieller Teiler tatsächlich ein Teiler ist
  • Aufsummieren der tatsächlichen Teiler, um festzustellen, ob sie die Zahl ergeben

Das sind zugegeben kleine Funktionseinheiten. Macht aber nichts. Jede, die Sie beim Nachdenken finden, ist eine gute. Denn damit bekommen Sie “Bausteine” an die Hand, die sie separat testen können. Das ist immer gut. Und Sie können womöglich entscheiden, in welcher Reihenfolge Sie deren Umsetzung angehen.

Ein zunächst monolithisches Problem zerfällt so in kleinere Probleme und die womöglich wiederum in kleinere usw. Nachdenken hilft also bei der Komplexitätsbewältigung.

Zu 3: Allerdings mag es sich herausstellen, dass Sie sich mit der Umsetzung der einen oder anderen Funktionseinheit, auf die Sie beim Nachdenken gestoßen sind, nicht 100%ig wohlfühlen. Das ist ganz normal. Es mag an der Problemdomäne liegen oder an einer Technologie, die zum Einsatz kommen soll. Deshalb ist es wichtig, dass Sie vor dem Codieren noch einen Zwischenschritt machen.

Seien Sie sensibel für Ihre Unsicherheiten und räumen Sie sie aus. Das können Sie durch das Studium von Fachliteratur tun. Oder Sie befragen den Kunden nochmal. Oder Sie programmieren etwas. Rotzen Sie Code raus (Spike Solution), um sich z.B. mit dem neuen O/R Mapper vertraut zu machen, bevor Sie damit Produktionscode schreiben. Oder für das Problem der vollkommenen Zahlen könnten Sie sich Erweiterungsmethoden anschauen, die es möglich machen, die Funktion IstVollkommen() so lesbar zu gestalten.

Solange Sie noch unsicher sind, sollten Sie nicht mit der Codierung beginnen. Ansonsten entsteht schnell akzidenzielle Komplexität, d.h. Komplexität, die nicht nötig ist, die Sie letztlich auch nicht wollen.

Wenn Sie etwas programmieren, um Ihre Unsicherheit abzubauen, dann schmeißen Sie es am Ende besser weg. Nehmen Sie Erkenntnisse mit ins Codieren, aber keinen Code. Auch sind solche Spike Solutions keine Prototypen. (Allerdings können Sie offizielle Prototypen natürlich immer noch mit dem Kunden vereinbaren. Dann teilen Sie Ihre Unsicherheit mit dem Kunden.)

Zu 4: Erst wenn Sie genau verstehen, was der Kunde braucht, einen Plan haben, wie Sie ihm das geben können, nicht mehr unsicher sind, erst dann sollten Sie mit dem Codieren beginnen. Das gilt aus meiner Sicht auch für TDD. Oder gerade für TDD! Denn immer wieder sehe ich bei Entwicklern, die mit TDD beginnen, dass sie Schwierigkeiten haben, sich zuerst Tests vorzustellen, bevor sie etwas implementiert haben.

Das liegt meiner Meinung nach daran, dass sie das Problem noch nicht genügend gut durchdrungen haben bzw. der Kunde keine Akzeptanztestfälle geliefert hat. Und/oder es liegt daran, dass sie unsicher sind, was eigentlich zu implementieren ist. Denn wer davon keine Vorstellung hat, der tut sich schwer damit, Erwartungen zu formulieren.

Der Effekt ist dann oft, dass TDD mit trivialen Tests oder Sonderfalltests begonnen wird. Die für den Kunden viel wichtigeren “happy day” Szenarien werden dadurch auf die Lange Bank geschoben. Klarheit für deren Implementation ergibt sich auch nicht aus solchen Tests. Geschäftig mit Tests zu beginnen kann also auch ein subtiles Symptom von Prokrastination sein.

Zusammenfassung

TDD ist eine zentrale Praktik für professionelle Softwareentwicklung. Gerade deshalb ist es nötig, sich ihr ganz bewusst zu sein und immer wieder zu fragen, ob sie schon optimal betrieben wird. Ein systematisches Vorgehen hilft dabei – gerade am Anfang.

Vor dem falschen Programmierparadigma schützt TDD aber natürlich nicht. Wir tun also gut daran, auch seine Grenzen bewusst zu sehen. TDD produziert also immer nur Code auf dem fachlichen Wissensstand seines Anwenders. Deshalb ist es gut, immer wieder über den Tellerrand (der Objektorientierung) zu schauen und sich mit anderen auszutauschen.

Sonntag, 21. März 2010

Generiert – Architekturcompiler für Event-Based Components

Da hab ich ja was angerichtet: Event-based Components ziehen immer größere Kreise. Immer mehr Entwickler “wollen es tun”. Selten habe ich soviel direktes positives Feedback zu einem Konzept bekommen. Irgendwie scheine ich da einen Nerv getroffen zu haben.

Bei allem Wohlwollen haben EBCs aber natürlich auch noch Schwachstellen. Die kommen durch das Feedback und das “Herumspielen” mit dem Konzept ans Licht. Eines ist die Verdrahtung. Die ist zwar technisch simpel, aber nervig zu implementieren. Weil nicht nur wenige Komponenteninstanzen in andere zu injizieren sind, sondern viele Pins der Komponenteninstanzen mit denen anderer verbunden werden müssen, steigt der Aufwand für die Laufzeitintegration. Dem stehen zwar große Gewinne gegenüber – doch nervig bleibt die Verdrahtung.

Besser würde die Lage natürlich, gäbe es einen hübschen EBC Designer à la LabVIEW.

image

Soweit sind wir aber noch nicht. Das braucht noch etwas mehr Erfahrung mit EBCs, glaube ich. Und auch Zeit, denn einen Designer zu basteln, ist kein Pappenstil.

Geht´s denn nicht aber auch ohne Designer schon ein bisschen einfacher? Einen “automatischen Verdrahter” hatte ich ja schonmal gebastelt. Der funktioniert aber nur in sehr einfachen Szenarien ausreichend. Wenn es komplizierter wird, reicht “Verdrahtung nach Konvention” nicht mehr aus. Dann müssten zusätzliche Informationen her. Dann wären wir wieder bei einer Architekturbeschreibung.

Also habe ich mir gedacht: Warum nicht mit einer Architekturbeschreibung ohne Designer anfangen? Also habe ich mal “rumgesponnen”. Erst habe ich mir ne textuelle DSL überlegt. Doch dann dachte ich, dass es noch besser wäre, noch klarer zu entkoppeln. Also bin ich auf das gute alte XML gekommen.

Wie wir am Ende Architekturen entwerfen und formulieren, ist egal. Eine graphische oder auch textuelle Notation wird sich immer in ein XML-Format übersetzen lassen. Das kann die Konstante in der Mitte sein zwischen Entwickler und Code.

Deshalb habe ich mir mal ein ganz einfaches Format überlegt, mit dem man einfache EBC-Architekturen beschreiben kann. Als Beispiel eine Stopuhr-Anwendung. Zuerst die Architektur gemalt in Visio:

image

Und hier die Übersetzung in das XML-Format. Ich nenne es mal ebclang für EBC Language:

image

Die Komponenten sind für sich mit ihren Output- und Input-Pins beschrieben, anschließend die Verdrahtung. Ich denke, das ist recht einfach zu verstehen. Geschachtelte Komponenten habe ich einstweilen allerdings noch außen vor gelassen.

Nachdem ich so eine XML-Architekturbeschreibung hatte, habe ich die von Hand ganz systematisch in Codeartefakte übersetzt. Das funktionierte sehr einfach. Es lässt sich gut automatisieren.

Deshalb habe ich in einem Anfall von Lust am “mud wrestling” ;-) einen Übersetzer dafür gebastelt. Der ist aus dem Stand gleich zu sehr hübschem Brownfield Code geworden. Aber das macht nichts. Er ist als Spike Solution gedacht, nicht mehr. Ich wollte mit dem Code nur das Feld der Artefaktgenerierung explorieren, bevor ich eine “richtige” Version entwickle.

Der ebclang.compiler übersetzt eine XML-Architekturdefinition wie oben in Assemblies und Visual Studio Projekte. Das sind im Einzelnen:

  • myapp.messages.dll und myapp.messages.csproj: Ein Projekt, das alle Nachrichtentypen implementiert. Der Compiler legt jeden Nachrichtentyp, der kein Standardtyp ist, darin als Klasse an.
  • myapp.specifications.dll: Eine Assembly, die die Komponenten in Form von Interfaces spezifiziert.
  • myapp.wiring.dll: Eine Assembly, die Komponenteninstanzen verdrahtet.

“myapp” steht für den Applikationsnamen wie im XML auf <MainBoard> angegeben. Die Spezifikation und das Wiring werden bewusst nur als Assemblies erzeugt, damit niemand auf die Idee kommt, sie zu verändern. Darin steckt die manifestierte Architektur. Die soll nur durch das XML veränderbar sein.

Die Nachrichtentypen jedoch müssen “nachgebessert” werden können. Deshalb darf man am messages-Projekt arbeiten. Der Compiler überschreibt Nachrichtentypquelldateien nicht.

Zusätzlich legt der Compiler noch Werkbänke für die Komponenten an. Das sind VS Projektmappen mit zwei Projekten, einem für die Komponentenimplementation und einem für deren Tests.

Das Vorgehen beim Entwurf einer EBC-Architektur ist mit dem Compiler wie folgt:

1. Anlegen eines Verzeichnisbaums für die Artefakte. Hier die Minimalstruktur:

image

Wurzelverzeichnis, darunter ein Verzeichnis lib/ mit nunit.framework.dll und weiteren für die Anwendung nötigen Bibliotheken. Und das Verzeichnis arc/, in dem die Architekturbeschreibung liegt. (Das Batch-File ruft den ebclang-Compiler für die XML-Datei auf.)

Nach Übersetzung der Architekturdefinition sieht der Artefaktbaum so aus:

image

Die .log-Dateien enthalten den C#-Compiler Output der Übersetzungen der myapp.* Projekte. Falls mal was schief gegangen sein sollte, darin nachschauen, bei welchem Generierungsschritt es gehakt hat. Der Compiler ist eine Konsolenanwendung und liefert auch noch Informationen während der Übersetzung.

Die Projekte *.specifications und *.wiring sollten nicht weiter beachtet werden. Der Compiler könnte sie genauso gut löschen. Ich habe sie bisher zur Fehlersuche aber noch drin gelassen. *.messagetypes enthält das Projekt, in dem die Nachrichtentypen “ausgefleischt” werden können.

Wichtig ist das bin-Verzeichnis in arc/:

image

Hier sind die Assemblies angekommen, die der Compiler erzeugt hat. Von hier müssen sie in allen weiteren Anwendungsprojekten referenziert werden. ebclang.basics.dll enthält bisher nur einen Nachrichtentypen für bidirektionale Kommunikation (Request<,>) und seine Erweiterungsmethoden.

Mehr ist eigentlich nicht nötig als Vorarbeit durch den Compiler. Auf der Basis der Assemblies kann man loslegen mit der Anwendungsimplementation. Die aufwändige Verdrahtung steckt ja in *.wiring.dll. Das EBC-Leben ist damit einfacher geworden.

Dennoch tut der Compiler etwas mehr. Er erzeugt auch noch Gerüstprojektmappen für die Komponenten im source/ Verzeichnis des Artefaktbaumes:

image

Diese Werkbänke geben den Rahmen vor, in dem Komponenten implementiert werden sollen. Erstens fokussieren sie den Blick, indem sie jede Komponente in eine eigene Projektmappe stellen. Zweitens geben sie vor, dass automatisiert getestet werden muss. Drittens setzen sie schon die Referenzen auf die generierten Assemblies und stellen den Output-Path auf ein globales bin-Verzeichnis.

Das mögen Kleinigkeiten sein. Doch in Clean Code Developer Seminaren, die viel mit Architektur zu tun haben, bemerken wir immer wieder, dass diese Kleinigkeiten viel Zeit kosten. Also habe ich versucht, hier eine erste Linderung zu bringen. Die ist noch nicht perfekt, weil z.B. keine Klasse für die Komponentenimplementation generiert wird, aber sie ist ein Anfang.

Was bleibt ist die Visualisierung von Architekturdefinitionen. Die soll ultimativ natürlich ein Designer übernehmen. Bis dahin wollte ich aber nicht warten. Also habe ich noch einen kleinen Visualisierer für die XML-Definitionen gebastelt. Den macht man einfach parallel zu Visual Studio oder einem anderen XML-Editor auf und lädt die Architekturdatei. Immer, wenn die sich verändert, aktualisiert man die Diagrammdarstellung und bekommt visuelles Feedback.

image

Die Visualisierung ist natürlich nicht interaktiv und auch nicht besonders hübsch. Aber sie erfüllt erstmal ihren Zweck, denke ich. Und der ist, dass – wer will – einen leichteren Einstieg in den Umgang mit Event-based Components bekommt.

Am Flipchart mit einer Architekturskizze beginnen, die in das XML-Format übersetzen und dabei die Übersetzung mit dem Visualisierer immer wieder überprüfen. Am Ende die Artefakte mit dem Compiler erzeugen und Nachrichtentypen sowie die Komponentenimplementationen in Visual Studio entwickeln.

Fehlt am Ende nur noch eines: Ein Programm, dass die ganzen Komponenten instanziert und die generierte Verdrahtung aufruft. So ein Programm – ich nenne es Host – generiert der Compiler noch nicht. Aber es ist schnell geschrieben. Dem Compiler-Download liegt es in der Nachher-Version der Beispielanwendung bei:

image

Mit einem DI Container wie Unity sieht ein Host z.B. so aus:

   19 static void Main()

   20 {

   21     Application.EnableVisualStyles();

   22     Application.SetCompatibleTextRenderingDefault(false);

   23 

   24     // Prepare Build

   25     IUnityContainer uc = new UnityContainer();

   26     uc.RegisterType<IPortal, FrmPortal>(new ContainerControlledLifetimeManager());

   27     uc.RegisterType<IStopuhr, Stopuhr>();

   28     uc.RegisterType<MainBoard, MainBoard>();

   29 

   30     // Build & Bind

   31     var mainboard = uc.Resolve<MainBoard>();

   32 

   33     // Run

   34     Application.Run((Form)uc.Resolve<IPortal>());

   35 }

Das Portal ist ein Singleton, damit es nicht zweimal instanziert wird. Einmal während der Injektion in das MainBoard und einmal am Schluss bei Run().

Wer diesen ersten Wurf für einen Architekturcompiler für EBCs interessant findet, der kann ihn hier herunterladen. Wie immer freue ich mich über Feedback.

Freitag, 26. Februar 2010

Bindungsenergie – Gedanken über ein Tooling für Event-Based Components

Der Gewinn, den Event-Based Components (EBC) bringen, steht nach meinem vorhergehenden Blogartikel inzwischen unzweifelhaft fest. Bevor EBC weitere Kreise ziehen können, muss es dafür aber ein Tooling geben, glaube ich. Dependency Injection kann man von Hand betreiben – aber das macht keinen Spaß, wenn die Anwendungen größer werden. Dasselbe gilt sogar potenziert für die Verdrahtung von EBC “Pins”. Deren Zahl ist viel größer als die der Interfaces, für die DI zuständig ist.

Wie könnte also ein Unterstützung von EBCs mit einem “Container” aussehen?

Die Grundsituation sieht so aus: Komponente Client und Service sind zusammen zu stecken. Client publiziert dafür einen Event, Service einen Event-Handler. Der Event ist der Output-Pin, der mit dem Event-Handler als Input-Pin verbunden werden muss. Zwischen beiden ist eine Leiterbahn zu legen.

Anmerkung: Ich benutze ab jetzt Begriffe aus der Elektrotechnik ohne Anführungszeichen. Platine, Pin, Bauteil, Leiterbahn usw. scheinen mir einfach so gut zu passen, dass ich sie nicht mehr als Analogie hervorheben will, sondern für den Moment mal zu EBC-Fachbegriffen mache.

Die EBC-Bauteilspezifikationen sehen dafür z.B. so aus:

interface IClient

{

    event Action<string> OnOutput;

}


interface
IService

{

    void ProcessString(string text);

}

Konkrete Bauteile, die diesen Spezifikationen folgen, können dann so zusammengesteckt werden:

IService s = new Service();

IClient c = new Client();

c.OnOutput += s.ProcessString;

Das ist ganz einfach. Aber wenn es um mehr als 2-3 Leiterbahnen geht, dann wird es lästig. Wie kann das also automatisiert werden?

Vom Nutzen eines DI Container

Zunächst dachte ich, ein DI Container hat bei EBC nicht mehr soviel Bedeutung. Doch das stimmt nicht. Er seinen vollen Wert für die Build Phase. In der werden die EBC-Bauteile instanziert. Die legt sie sozusagen auf die Werkbank, bevor sie in der Bind Phase auf einer Platine zusammengesteckt werden.

Im obigen Beispiel besteht die Build Phase aus den beiden Instanzierungen. Mit einem DI Container kann die so aussehen:

IUnityContainer uc = new UnityContainer();

uc.RegisterType<IService, Service>();

uc.RegisterType<IClient, Client>();

IService s = uc.Resolve<IService>();

IClient c = uc.Resolve<IClient>();

Obwohl die EBC-Komponenten keine funktionalen Abhängigkeiten untereinander haben, lohnt sich der Einsatz eines DI Containers. Denn zum einen können Bauteile von anderer Funktionalität abhängen, die nichts mit EBC zu tun hat. Zum anderen können Bauteile Platinen sein und insofern doch von Bauteilen abhängen. Davon später mehr.

Wenn Sie sich auf EBC einlassen, vergessen Sie Ihren liebsten DI Container also nicht! Instanzieren Sie die EBC-Bauteile mit ihm.

Bauteile binden

Die Herausforderung für das Tooling rund um EBCs liegt also nicht in der Instanzierung, sondern in der automatischen Verbindung von Pins, d.h. in der Bind Phase. Die besteht im Beispiel nur aus einer Zeile:

c.OnOutput += s.ProcessString;

Wenn das nun mehr Zeilen werden, wie könnte Ihnen ein Tool die Arbeit abnehmen? Ich stelle mir das derzeit so vor:

ComponentBinder.Bind(c, s);

Schön einfach, oder? So soll es ja auch sein. Die Bind()-Methode soll halt irgendwie dafür sorgen, dass die Input- und Output-Pins der übergebenen Bauteile “zu einander finden”. Ganz allgemein sollen Sie also Bind() Instanzen aller zu verdrahtenden Bauteile übergeben. Wieviele das sind, hängt vom Abstraktionsniveau des Codes ab, in dem die Bindung stattfindet.

Sie können sich z.B. entscheiden, nur mit atomaren Bauteilen zu arbeiten. Dann rufen Sie Bind() nur einmal mit all diesen Bauteilen auf. Für die Bauteile

A->B->C->D

sähe das so aus: Bind(a, b, c, d)

Wenn Sie allerdings Bauteile auf Platinen zu größeren Einheiten aggregieren, dann sieht das anders aus:

A->Platine(B->C)->D

würde zu den Aufrufen Bind(a, platine, d) und Bind(b, c) führen.

Aufgerufen wird Bind() immer von einer Platine. Im einfachsten Fall gibt es nur eine für Ihre ganze Anwendung. Sobald die Sache jedoch etwas größer wird, werden Sie Bauteile auf kleineren Platinen zusammenfassen und sogar Platinen wieder auf größeren zusammenstecken. Davon gleich mehr.

Wie kann eine automatische Bindung von Output- mit Input-Pin stattfinden? Die Pins haben folgende Grundform:

Output: event Action<T> _
Input: void _(T _) {…}

Die Unterstriche stehen hier für Angaben, die für die Bindung zunächst nicht relevant sind. Im ersten Anlauf würde ich einfach mal alle Methoden mit 1 Parameter vom Typ T und ohne Rückgabetyp als Event-Handler bei allen Events vom Typ Action<T> registrieren.

image

Jedem Event sind also potenziell mehrere Event-Handler zugeordnet:

image

Und falls es mehrere Events vom selben Typ gibt, dann sind die Event-Handler mit all diesen Events verbunden:

image

Wer solches Leiterbahnenspaghetti vermeiden möchte, der muss einfach nur zusehen, dass sich die Typen der Events konsequent unterscheiden und dass es für jeden Event-Typ nur einen Handler gibt.

Soweit eine erste einfache Bindungsalgorithmusversion. Eine zweite Version könnte dann auch noch die Namen der Pins mit einbeziehen. Dann würden nur Pins verbunden, deren Typen übereinstimmen und (!) bei denen auch irgendwie die Namen passen. Dazu braucht es eine Konvention. Die könnte so aussehen:

Events haben den Präfix “On”, Event-Handler der Präfix “Process” oder “Handle”. Pins werden zusammengesteckt, wenn sie im Typ übereinstimmen und in dem, was auf den jeweiligen Präfix folgt:

image

Dem Nachrichtentypnamen ist sozusagen noch ein Pinname zugeordnet. Im vorstehenden Bild sind das T.X und T.Y.

Das scheint mir zunächst auszureichen. Das Matching der Output- mit den Input-Pins kann dann so ablaufen:

  1. Sammle alle Output-Pins
  2. Sammle alle Input-Pins
  3. Für jeden Output-Pin…
    1. Finde alle Input-Pins, die zu seinem “qualifizierten Typ” (Typ + Pinname) passen
    2. Wenn es solche Pins gibt, dann registriere sie als Event-Handler…
    3. …ansonsten suche alle Pins, die nur zu seinem Typ passen
      1. Wenn es solche Pins gibt, dann registriere sie als Event-Handler…
      2. …ansonsten hängt der Output-Pin in der Luft. Was tun? Ich denke, das ist eine Fehlermeldung wert – es sei denn, auf die wird ausdrücklich verzichtet.

Schritte 1. und 2. laufen auf allen an Bind() übergebenen Bauteilen ab. Alle Bauteile sind gleich. Wie schon in einem früheren Posting geschrieben, gibt es bei EBC formal keine Client-Service-Abhängigkeiten. Dadurch ist es auch möglich, eine Komponente mit sich selbst zu verbinden. Rekursionen sind also auch möglich.

Platinen

Platinen sind ebenfalls EBC-Komponenten. Sie erfüllen allerdings keine Funktion im Sinne einer Geschäftslogik. Ihre Verantwortlichkeit ist allein die Aggregation von Bauteilen. Platinen bestehen daher vor allem aus einem Konstruktor, in dem die Platinenbauteile zusammengesteckt werden.

Hier ein simples Szenario:

image

Das Bauteil in der Mitte empfängt T-Nachrichten und erzeugt S-Nachrichten. Ob es atomar ist und die Transformationvon T nach S selbst vornimmt oder “nur” eine Platine ist, die andere Bauteile zu diesem Zweck aggregiert, das ist für die anderen Bauteile nicht erkennbar und auch nicht wichtig. Womöglich verändert sich das auch über die Zeit. Die Spezifikation bleibt gleich:

interface IMittelteil

{

    void ProcessX(string _);

    event Action<int> OnY;

}

Im Falle einer Platine sieht die Implementation allerdings speziell aus. Nehmen wir mal an, dass auf dem Mittelteil als Platine zwei andere Bauteile stecken:

image

Dann sähe die Implementation des Mittelteils mindestens so aus:

class Platine : IMittelteil

{

    public Platine(IB b, IC c)

    {

        …

        ComponentBinder.Bind(b, c);

    }

    …

Die Platine sorgt dafür, dass ihre Bauteile verdrahtet werden. Welche Bauteile das sind, bekommt sie über DI mitgeteilt. Hier tut also der DI Container wieder gute Dienste.

Auch wenn die Aufgabe der Platine denkbar simpel ist (und sie daher viele Abhängigkeiten haben darf), so ist sie wie oben noch nicht vollständig. Ihre Bauteile sind zwar untereinander verdrahtet – aber es fehlt die Verbindung zu den Pins der Platine. Der Input in die Platine muss ja in ihr Bauteil B fließen und Output aus C muss an die Umwelt der Platine weitergereicht werden.

Der Input-Event-Handler der Platine muss dazu mit dem Input-Event-Handler von B verbunden werden. Und der Output-Event von C muss den Output-Event der Platine feuern:

class Platine : IMittelteil

{

    private IB b;


    public
Platine(IB b, IC c)

    {

        this.b = b;

        this.c.OnY += x => this.OnY(x);

        …

    }


    public
void ProcessX(string _)

    {

        this.b.ProcessX(_);

    }


    public
event Action<int> OnY;

}

Inputs einer Platine müssen an Inputs von Bauteilen weitergeleitet werden, unverdrahtete Output-Pins von Bauteilen müssen verbunden werdne mit Output-Pins der Platine.

Das ist – wie gesagt – nicht schwierig und sehr regelmäßig - dennoch ein wenig nervig. Im Augenblick sehe ich allerdings noch keinen Weg, um das zu automatisieren. Die Verbindung von Bauteil-Outputs mit Platinen-Outputs wäre möglich, weil dafür nur ein Delegat ad hoc erzeugt werden muss (s. Lambda Funktion im Ctor). Aber was tun mit der Weiterleitung vom Platinen-Event-Handler zum Bauteil-Event-Handler? Vielleicht ist da etwas zu machen, wenn Platinen abstrakte Basisklassen sind oder ihre Event-Handler virtuelle Methoden? Dann könnte man von ihnen zur Laufzeit ableiten und die Event-Handler überschreiben. Hm… darüber muss ich mal nachdenken. Für den Anfang kann ich allerdings auch ohne eine Automatisierung dieses Aspektes der Verdrahtung von EBCs leben.

Injektionen

Wenn eine automatische Bindung grundsätzlich funktioniert, dann wäre der nächste Schritt, in diesen Prozess eingreifen zu können. Ich könnte mir vorstellen, dass ein ComponentBinder Events feuert, wenn er dabei ist, Pins zu verbinden. Auf diesen Events könnten Sie lauschen und eingreifen. Eine Verdrahtung könnte unterdrückt werden. Oder sie könnten einen anderen Event-Handler-Delegaten zurückreichen (z.B. einen für einen Tracer).

Alternativ könnten dem ComponentBinder Prädikate mit anhängenden Kommandos mitgegeben werden. Bei jeder bevorstehenden Bindung könnten die Prädikate geprüft und bei Wahrheit ihre Kommandos ausgeführt werden.

In jedem Fall scheint mir die Injektion von Zwischenstücken zwischen Output- und Input-Pins, wenn sie denn von Hand vorgenommen wird, keine so große Sache. In einer ersten Version eines Binders würde ich sie dennoch raus lassen.

Automatische Dokumentation

Ein interessanter Aspekt ist mir noch zur automatischen Bindung eingefallen: Der Binder “sieht” ja alle Bauteile. Warum sollte er dann nicht auch Auskunft geben darüber, welche das sind und wie er sie verdrahtet hat? Konkret: Warum sollte der Binder nicht eine Dokumentation generieren können über die Schachtelung und Verbindung von Bauteilen? In einem XML-Format ausgegeben könnte daraus anschließend eine Visualisierung der de facto Architektur generiert werden. Wie wäre das?

DI Container hätten das auch immer schon tun können. Haben sie aber nicht. Schade. So kann es denn ein ComponentBinder von vornherein besser machen.

PS: Jetzt hab ich es doch schon getan. Eine erste Version eines Binders für EBCs ist online bei CodePlex unter http://ebcbinder.codeplex.com:

image

Probieren Sie den Binder mal aus. Dann diskutieren wir im Forum des Projektes, wie es damit weitergehen kann.

Dienstag, 23. Februar 2010

Verbindungsstücke – Event-Based Components abhören

Auf zum vorläufigen Endspurt mit den Event-Based Components (EBC). Wie die grundsätzlich definiert werden, habe ich hier beschrieben. Dann ging es darum, wie sie grundsätzlich zusammengesteckt werden. Und schließlich galt es, die Kommunikation noch etwas zu vereinfachen. Um mit EBC zu arbeiten, ist jetzt alles auf dem Tisch.

Aber es kommt noch besser! Denn bisher haben Sie nur gesehen, wie EBC das leisten, was “traditionelle” Komponenten auch leisten. Die EBC tun das natürlich architekturell sauberer, finde ich, aber funktional haben sie keinen Vorsprung. Das möchte ich nun ändern. Dazu müssen wir uns nochmal ansehen, wie EBC “zusammengesteckt” werden.

Ein ganz einfaches Szenario soll da genügen. Komponente Quelle verschickt eine Nachricht Output und Komponente Senke empfängt sie.

image

Hier die Kontrakt-Interfaces und die Nachricht:

interface IQuelle

{

    event Action<Output> OnOutput;

}


interface
ISenke

{

    void ProcessOutput(Output msg);

}


class
Output

{…}

Die Bindung im Rahmen einer “Software-Platine” geschieht dann so:

// Build

IQuelle q = new Quelle();

ISenke s = new Senke();

// Bind

q.OnOutput += s.ProcessOutput;

Anschließend kann die Quelle angestoßen werden und kommuniziert mit der Senke:

((Quelle) q).Run("hello");

…

class Quelle : IQuelle

{

    public void Run(string text)

    {

        this.OnOutput(new Output {Text = "<"+text+">"});

    }


    public
event Action<Output> OnOutput;

}

Alles easy. Nichts neues, wenn Sie die bisherigen Blogartikel zu EBC verfolgt haben. Jetzt halten Sie sich aber fest…

Nachrichten abfangen

Haben Sie schon mal versucht, die Kommunikation zwischen “traditionellen” Komponenten zu verfolgen? Vielleicht ist eine komponentenorientierte Anwendung mal nicht so gelaufen, wie Sie wollten, und Sie haben gedacht, wenn Sie wüssten, mit welchen Argumenten die Komponenten sich gegenseitig aufrufen, dann könnten Sie leicht heraus finden, wo es hakt, ohne zum Debugger zu greifen.

Ich jedenfalls möchte so ein Tracing in Anwendungen einschalten können. Natürlich ohne dafür eine Änderung in meinem Quellcode vornehmen zu müssen. Das funktioniert auch mit einem DI Container wie Structure Map und automatisch generierten Proxies. Stefan Lieser hat das mal für die Anwendungen gemacht, die wir innerhalb der Clean Code Developer Seminare mit den Teilnehmern entwickeln. Dafür war allerdings schon einiges Spezialwissen nötig.

Mit EBC geht das hingegen ganz einfach. Wenn Sie auf einer Verbindung zwischen zwei EBC lauschen wollen, um z.B. den Nachrichtenfluss zu protokollieren, dann tun Sie das ganz einfach so:

// Bind

q.OnOutput += o => Console.WriteLine("tracing {0}", o);

q.OnOutput += s.ProcessOutput;

Registrieren Sie einen zweiten Event-Handler am Output-“Pin” einer EBC. Der empfängt alle Nachrichten wie die eigentliche Zielkomponente für die Nachrichten. Schließlich sind Events multi-cast Delegaten.

image

Wenn Sie den Lauscher vor der Zielkomponente registrieren, bekommt er zuerst die Nachrichten. Registrieren sie ihn hinterher, dann bekommt er die Nachrichten erst, wenn die Zielkomponente schon fertig ist. Das können Sie auch dynamisch zur Laufzeit tun. Stellen Sie das Abfangen für jeden Output-“Pin” separat ein und aus, wenn Sie mögen.

Nix Reflection, keine dynamischen Proxies, kein Hexenwerk… alles ganz einfach mit dem Abfangen von Nachrichten bei EBC (Interception). EBC schenkt Ihnen sozusagen Aspektorientierte Programmierung (AOP) für manche Szenarien.

Verbindungsstücke

Denken Sie das noch ein Stück weiter: Die Verbindung zwischen Quelle und Senke ist explizit für jede Nachricht. Output-“Pin” wird mit Input-“Pin” “verdrahtet”… Wer sagt da eigentlich, dass diese “Drähte” direkt zwischen den Komponenten verlaufen müssen?

Sie könnten ein Tracing in Form einer generischen Funktionseinheit als Zwischenstück in die Verbindung zwischen Quelle und Senke einsetzen:

image

Das ist bei der Bindung ja leicht möglich:

q.OnOutput += Tracer.Create<Output>(s.ProcessOutput);

Der Tracer ist dann nichts als eine Indirektion zwischen Output-“Pin” und Input-“Pin”:

class Tracer

{

    public static Action<T> Create<T>(Action<T> processor)

    {

        return t =>

            {

                Console.WriteLine("tracing: {0}", t);

                processor(t);

            };

    }

}

Wo hier Console.WriteLine() steht, können Sie natürlich beliebig viel Aufwand für ein generisches Tracing treiben – oder Sie delegieren die Aufgabe an log4net oder SmartInspect.

Oder überlegen Sie ein anderes Szenario für ein Zwischenstück. Wie wäre es, wenn Sie Nachrichten zur Laufzeit aufhalten wollen, bis eine Senke sie (wieder) aufnehmen kann? Sie könnten ein Ventil zwischen Quelle und Senke einsetzen.

image

Das ist bei der “Verdrahtung” ganz einfach:

var valve = new Valve<Output>();

q.OnOutput += valve.Register(s.ProcessOutput);

Weder Quelle noch Senke merken etwas davon, dass der Fluss zwischen ihnen über das Ventil gesteuert werden kann:

((Quelle)q).Run("1");

valve.Close();

((Quelle)q).Run("2");

((Quelle)q).Run("3");

valve.Open();

Die Nachrichten “2” und “3” werden erst bei Aufruf von Open() an die Senke weitergeleitet. Und die Implementation eines solchen Ventils ist trivial:

class Valve<T>

{

    private bool isOpen = true;

    private Queue<T> buffer = new Queue<T>();

   
    public void ProcessMessages(T msg)

    {

        if (this.isOpen)

            this.OnMessage(msg);

        else

            this.buffer.Enqueue(msg);

    }


    public
event Action<T> OnMessage;


    public
Action<T> Register(Action<T> processor)

    {

        this.OnMessage += processor;

        return this.ProcessMessages;

    }


    public
void Open()

    {

        foreach (T msg in this.buffer)

            this.OnMessage(msg);

        this.isOpen = true;

        this.buffer.Clear();

    }


    public
void Close()

    {

        this.isOpen = false;

    }

}

Jetzt denken Sie mal weiter… Was ließe sich mit so einem “Ventil” oder einem vergleichbaren Steuerelement zwischen Komponenten noch alles machen? Sie könnten Ziel-Komponenten dynamisch laden und austauschen. Sie könnten Ziel-Komponenten ihren Zufluss selbst steuern lassen. Sie könnten Load-Balancing betreiben. Sie könnten Nachrichten per TCP verschicken zu einer entfernten Komponente… Das alles und noch mehr würde Nachrichten 1:1 weiterleiten. Früher oder später ;-)

Jetzt denken Sie noch weiter… Was könnten Sie mit den Nachrichten in “Zwischenstücken” alles machen? Sie könnten Sie transformieren, filtern, zerlegen, aggregieren… Und wieder alles, ohne dass die Komponenten selbst davon etwas merken würden.

Wer schon mal davon geträumt hat, wiederverwendbare Komponenten zu entwickeln, der hat jetzt endlich etwas in der Hand. Nicht Geschäftslogik-Komponenten sollten auf Wiederverwendbarkeit getrimmt werden, sondern Infrastrukturkomponenten wie ein solches Ventil. Dafür gibt es aber erst einen Markt, wenn Sie Ereignisorientierung und Nachrichtenorientierung denken.

Solch flexibler Umgang mit nachrichtenindividuellen Komponentenverbindungen ist für mich dann auch der Grund, Nachrichten einen eigenen Typ zu geben. Dann sehen nämlich alle Verbindungen zwischen Komponenten gleich aus. Sie sind vom Typ Action<T>. Und dann kann man generische Infrastruktur bauen, die mit diesen T-Nachrichten etwas tut.

Wer da an Enterprise Integration Patterns denkt, der liegt nicht falsch. Mir gehts aber erstmal nur um eine Flexibilisierung der synchronen Kommunikation zwischen Komponenten.

Jetzt sind Sie dran. Wohin trägt Sie Ihre Phantasie nun, da sie Event-Based Components kennengelernt haben?

Mustergültig – Event-Based Components aufbohren

Wie Sie Event-Based Components (EBC) grundsätzlich “zusammenstecken” habe ich hier gezeigt. Das ist nicht schwer, oder? Nur etwas gewöhnungsbedürftig. Dafür winken aber einige Vorteile gegenüber “traditionellen” Komponenten, finde ich. Meine “Bauchschmerzen”, die ich mit denen bei aller Liebe immer noch latent hatte, sind jetzt weg.

Ob ich Sie damit jedoch automatisch auch für EBC begeistert habe, weiß ich nicht. Das streng nachrichtenorientierte Programmiermodell und die “Verklausulierung” von Serviceaufrufen als Events sind recht weit weg vom Üblichen. Ich will deshalb hier noch ein paar Kommunikationsmuster beschreiben, die Ihnen das Konzept schmackhafter machen.

One-way Nachrichten

Das Grundmuster der EBC-Kommunikation ist die one-way Nachricht. Als solche bezeichne ich eine Nachricht, die vom Empfänger keine Antwort an den Absender erwartet. Bisher habe ich nur Beispiele für one-way Nachrichten gegeben. Selbst die Nachricht ParseResult, die vom Parser als Antwort zurück fließt zum CompilerCoordinator, ist eine solche one-way Nachricht:

interface ICompilerCoordinator

{

    …

    event Action<ParseRequest> OnParse;

    void ProcessParseResult(ParseResult result);

}


interface
IParser

{

    void ProcessParseRequest(ParseRequest request);

    event Action<ParseResult> OnParseResult;

}

Formal besteht zwischen ParseRequest und ParseResult kein Unterschied. Die Semantik, dass ein ParseResult als Ergebnis eines ParseRequest versandt wird, ist nur in unserem Kopf. Deshalb sind beide Nachrichten formal gleich.

Wie gesagt: Die Kommunikation zwischen Interaktionspartnern ist bei der Ereignisorientierung grundsätzlich symmetrisch.

One-way Nachrichten sind im Sinne der Command Query Separation das Mittel, um Kommandos zu versenden. Die Implementation ist denkbar einfach: Der Sender definiert einen Event für eine one-way Nachricht als Output; der Empfänger definiert einen passenden Event-Handler. Auf einer “Software-Platine” werden beide mit einander verbunden.

One-way Sender:

event Action<T> OnT;

One-way Empfänger:

void ProcessT(T msg);

Request/Response V1

Auch wenn es in der Ereignisorientierung formal nur one-way Nachrichten gibt, hilft es ja nichts: viele Interaktionen erwarten eine Antwort. Request/Response ist ein typisches Muster in der Kommunikation zwischen Funktionseinheiten. Dafür sind Funktionen gemacht. Auch die obige ParseRequest-Nachricht ist ein solcher Aufruf, der die Produktion eines Resultats anstoßen soll. Ansehen tun sie ihm das aber nicht. Das ist misslich, weil der ereignisorientierte Code dadurch schlechter lesbar ist als üblicher methodenbasierter.

Das können Sie jedoch leicht ändern, indem Sie den Request “aufbohren”. Sie verschicken mit ihm nicht nur Argumente für die Bearbeitung – sondern auch noch den Empfänger für die Antwort:

class ParseRequest

{

    public string Source;

    public Action<ParseResult> ProcessParseResult;

}

Jetzt wird die Nachricht ihrem Namen gerecht. Sie ist ein echter Request, eine Anforderung von Daten. Source steht für die Input-Daten beim Empfänger und die Methode ProcessParseResult für den Empfänger des Outputs.

Die Kontrakte für CompilerCoordinator und Parser werden dadurch einfacher:

interface ICompilerCoordinator

{

    void ProcessCompilationRequest(CompilationRequest request);

    event Action<ParseRequest> OnParse;

}


interface
IParser

{

    void ProcessParseRequest(ParseRequest request);

}

Der Produzent von Resultaten braucht bei solchen Aufträgen keinen “Pin” mehr eigens für ihren Versand. Das macht ihn auch flexibler, weil er dadurch von vielen Clients als Service benutzt werden kann. Jeder Client schickt ja mit seinem Request einen Request-individuelle Antwort-“Pin” mit. Der funktioniert wie eine Return-Adresse bei normalen Funktionsaufrufen.

class CompilerCoordinator : ICompilerCoordinator

{

    public void ProcessCompilationRequest(CompilationRequest request)

    {

        ParseResult pr;

        this.OnParse(new ParseRequest

            {

                Source = request.Source,

                ProcessParseResult = r => pr = r

            });

        …

    }

    …

Im einfachsten Fall stecken Sie in den nachrichtenindividuellen Event-Handler für das Resultat eine Lambda-Funktion wie hier gezeigt. Ja, das ist etwas umständlicher als ein Funktionsaufruf – aber ich glaube immer noch daran, dass am Ende der Gewinn aus solch ereignisorientierter – oder besser: nachrichtenorientierter – Kommunikation die Umständlichkeit mehr als kompensiert. Außerdem ist der Umstand ja nur für inter-komponenten Aufrufe zu treiben. Die stellen gewöhnlich nur eine Minderheit in Ihrem Code dar.

Das Muster einfach: Die Request-Nachricht bekommt einen eigenen Event (oder auch nur einen Delegate), den ihr Empfänger mit einem Resultat aufruf. Genersich sieht das z.B. so aus:

class Request<TInput, TOutput>

{

    public TInput Input;

    public Action<TOutput> ProcessResult;

}

Client:

event Action<Request<T0, T1>> OnT0WaitingForT1;

Service:

void TransformT0IntoT1(Request<T0, T1> request);

Notifkationen

Den Empfänger in einer Nachricht zu transportieren ist der universelle Weg, um Nachrichten kontextabhängig zuzustellen. Das gilt nicht nur für Resultate, sondern auch für Zwischenergebnisse jeder Art. Das können z.B. Fortschrittsinformationen sein. Wenn Sie den Fortgang einer Operation dem Auslöser mitteilen möchten, versenden Sie am besten einen Endpunkt, dem darüber Bescheid gegeben werden kann:

class ParseRequest

{

    …

    public Action<double> ShowProgress;

}

Ein Service muss dann nicht permanent an einen Event-Handler gebunden sein, sondern bekommt für jeden Auftrag womöglich einen neuen mitgeteilt.

ParseResult pr;

this.OnParse(new ParseRequest

    {

        Source = request.Source,

        ProcessParseResult = r => pr = r,

        ShowProgress = p => Console.WriteLine("Fortschritt: {0}", p)

    });

Hier zeigt sich für mich sehr schön die Simplizität der Nachrichtenorientierung. Sie kennt nur wenige Konzepte, die man aber ganz flexibel immer wieder neu kombinieren kann. In einer einführenden Serie zum Application Space in meinem englischen Blog können Sie sehen, was das für verteilte Anwendungen bedeutet. Notifikationen sind sozusagen “first class citizens” bei der Nachrichtenorientierung.

Das Muster ist ganz einfach und unterscheidet sich nicht von dem für Resultate in Request/Response-Szenarien.

Request/Response V2

Ich verstehe, wenn Ihnen eigene Events für Resultats oder Notifikationen nicht so leicht runtergehen. Sie sollten sie allerdings als Muster “drauf haben”. Allemal für die synchrone ereignisbasierte Kommunikation sehe ich jedoch noch eine Alternative. Wir können einen Delegaten speziell für Requests definieren:

delegate void Request<TInput, TOutput>(TInput message,

Action<TOutput> processResult);

Der Kontrakt wird damit wieder einfacher:

  1. Es ist kein Resultat-Event in der Request-Nachricht mehr nötig.
  2. Die Signaturen können sich auf das Wesentliche konzentrieren und werden verständlicher.

Hier der überarbeitete CompilerCoordinator als Client:

interface ICompilerCoordinator

{

    …

    event Request<string, ASTNode> OnParseRequest;

}

Ich habe jetzt sogar auf den ParseRequest-Nachrichtentypen verzichtet, weil darin ohnehin nur ein Argument verpackt würde. Die Gesamtsignatur scheint mit jetzt so spezifisch, dass der Typ nicht mehr nötig ist. (Allerdings würde ich immer noch dazu raten, nicht mehr als einen Parameter für die Argumente einer Operation zu benutzen. Fallen Sie nicht zurück in die Gewohnheiten “traditioneller” Komponenten mit ihren ausgefeilten Signaturen. Sie machen es sich damit unnötig schwer, später auf die asynchrone Ereignisorientierung umzusteigen.)

Und hier der Parser als Service:

interface IParser

{

    void ProcessParseRequest(string source, Action<ASTNode> processResult);

}

Beachten Sie, wie der Endpunkt für den Versand des Resultats als Delegat in den Event-Handler hinein gereicht wird. Das ist besser lesbar, weil schon beim Blick auf die Signatur klar ist, dass ein Resultat geliefert werden soll.

Und auch der Client wird in der Implementation einfacher:

class CompilerCoordinator : ICompilerCoordinator

{

    public void ProcessCompilationRequest(CompilationRequest request)

    {

        ASTNode program;

        this.OnParseRequest(request.Source, r => program = r);

        …

    }

    …

Das Muster ist einfach: Für jeden kontextspezifischen Notifikationsendpunkt übergeben Sie einen an den Nachrichtenempfänger Delegaten als Parameter. Der kann dann ein Resultat liefern oder Fortschrittsinformationen oder noch ganz anderes…

Request/Response V3

Wenn Sie spezielle Delegaten für Requests denken können, dann können Sie das Spiel natürlich auch noch weiter treiben. Auch Folgendes ist möglich:

public void ProcessCompilationRequest(CompilationRequest request)

{

    ASTNode program = this.OnParseRequest.Request(request.Source);

    …

}

Huch! Wo ist denn die Ereignisorientierung hin? Der Code feuert den Event mit der Aufforderung zum Parsen, denn er benutzt ja dessen Membervariable OnParseRequest. Aber die Aufforderung liefert ein Resultat. Wie kann das sein?

Eine Erweiterungsmethode auf dem Request<>-Delegatentyp machts möglich:

public static class RequestExtension

{

    public static TOutput Request<TInput, TOutput>(this Request<TInput, TOutput> fireEvent, TInput msg)

    {

        TOutput output = default(TOutput);

        fireEvent(msg, r => output = r);

        return output;

    }

}

Und schon ist sie dahin die schöne Nachrichtenorientierung…

Auch wenn das möglich ist, empfehle ich doch, es nicht zu tun. Sie verschleiern das grundlegende Programmiermodell bis zur Unkenntlichkeit. Das halte ich für Kontraproduktiv im Sinne der Verständlichkeit. Man sieht einfach nicht mehr so deutlich, wo Komponentengrenzen verlaufen. Und es baut eine Hürde für die spätere Migration zu asynchroner Kommunikation auf.

Mit einer anderen Variante könnte ich mich allerdings anfreunden:

public void ProcessCompilationRequest(CompilationRequest request)

{

    this.OnParseRequest

        .Request(request.Source)

        .Receive(astNode =>

            {

                …

            });

}

Hier wird dem Request explizit eine Continuation mitgegeben, d.h. Code, der nach seiner Erfüllung ausgeführt werden soll. So ist der Code immer noch lesbar; die bisherige lokale Variable für das Resultat wird gespart; und vor allem funktioniert dieser Stil auch in asynchronen Szenarien.

Möglich wird mit einer Erweiterungsmethode und einer kleinen Fluent Interface Klasse:

public static class RequestExtension

{

    public static RequestContinuation<TOutput> Request<TInput, TOutput>(this Request<TInput, TOutput> fireEvent, TInput msg)

    {

        return new RequestContinuation<TOutput>(processResult =>

            fireEvent(msg, processResult));

    }


     public
class RequestContinuation<TOutput>

    {

        private readonly Action<Action<TOutput>> fireEvent;

    
       
public RequestContinuation(Action<Action<TOutput>> fireEvent)

        {

             this.fireEvent = fireEvent;

        }

   
        public void Receive(Action<TOutput> processResult)

        {

            fireEvent(processResult);

        }

    }

}

Hervorhebenswert ist hier, dass die Erweiterungsmethode eine Funktion höherer Ordnung aus dem Event-Delegaten macht. Der wird also nicht in der Erweiterungsmethode aufgerufen, sondern erst in Receive(). Ein wenig Funktionale Programmierung ist hier also hilfreich, um die Ereignisorientierung angenehmer zu gestalten.

Soweit für dieses Mal mit den Event-Based Components. Ich hoffe, inzwischen konnte ich Ihre Bedenken zerstreuen, dass die unhandlich und schwierig sind. Beim nächsten Mal geht es weiter mit einem Blick auf die Verdrahtung von EBCs.