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.
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.
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:
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);
};
}
}
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.
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?
9 Kommentare:
Finde das alles hochspannend. Event based components...hm... *hirn*.
Etwas provokativ gefragt - wieso nicht grad einen Service Bus lokal einsetzen? Geht ja auch relativ schmerzfrei und ich habe die selben Vorteile - nur halt mit entsprechender Infrastruktur...?
@Laurin: Wie kommunizieren die synchronen Komponenten über den ServiceBus? Synchron? Asynchron? Über den Stack oder mittels Streams?
EBC sind genauso performant wie "traditionelle" Komponenten. Und sie sind genauso synchron. Beides würdest du mit einem intra-Prozess ServiceBus verlieren.
EBC sind ein Migrationspfad. Mit ihnen kannst du in einem Programmiermodell arbeiten, das dir den Übergang zu async lokal und dann async distributed leichter macht.
Warum sollte ich mir einen ServiceBus aufhalsen, wenn ich aber beides nicht brauche?
-Ralf
Fantastisch!
Ich habe mich jetzt eine Weile mit EBC beschäftigt und bin nach wie vor begeistert. Ich frage mich nur, was spricht gegen Komponenten mit mehr als einem In- und/oder Output? Ich habe mal eben ein Calculator-Demo geschrieben das aus folgenden Komponenten besteht: Keypad (0 Inputs, 4 Outputs (für jede Grundrechenart einen)), Calculator (4 Inputs, 1 Output), Display (1 Input, 0 Outputs) und was soll ich sagen, das Ding funktioniert auf Anhieb einwandfrei.
Gruß,
Rainer
@Rainer: Freut mich sehr, dass dir die EBCs immer noch gefallen. Ich find sie jeden Tag auch cooler :-)
Bei deiner Frage weiß ich leider nicht, was du meinst mit "was spricht gegen Komponenten mit mehr als einem In- und/oder Output".
Komponenten können natürlich beliebig viele Input- und Output-Pins haben. Output-Pins sind Events, Input-Pins sind Event-Handler.
Allerdings: Ich bin fest der Meinung, dass jeder Pin nur 1 Parameter haben sollte.
Technisch geht es auch anders. Klar. Events können mehrere Parameter haben, Event-Handler auch.
Ich halte es jedoch für sehr sinnvoll, diese Freiheit an der Schnittstelle von Komponenten zu beschneiden. Sozusagen freiwillige Selbstkontrolle ;-)
"Traditionelle" Komponenten sind FSK 16 :-) EBCs sind FSK 6 :-) Jüngeres Publikum (unerfahrenere Entwickler) können sie benutzen, weil sie mehr Regeln vorgeben.
Dieses Mehr an Regeln ist eine gewisse Standardisierung. Und Standardisierung erlaubt dann auch Standardbauteile. Die ganze Entwicklerwelt sucht ja ständig nach sowas, nach Wiederverwendung.
Na, dann bitteschön :-) Hier ist eine Chance dafür: Wenn Pins nur 1 Parameter haben, dann kann man alle möglichen Standardkomponenten basteln, die sich an solche Pins ansetzen lassen.
-Ralf
Hm.. Faszinierend. Event-based Kommunination auf einer neuen Skala: bislang aus grossen SOA-Systemen (high-level) oder als Mittel für Inter-Objekt Kommunikation (low-level) bekannt. Auf Komponenten-Ebene ein wirklich besonders mächtiges Konzept.
Ich finde das alles ebenfalls hochinteressant. Neue Architekturmuster, andere Möglichkeiten, effizientere Programmierung usw. sind immer gute Optionen.
Nur: Bei Mir bringt das alles rein beruflich nichts, da mein Projektleiter nicht einmal weiß was das Wort "Design Patterns" bedeutet und sich hartnäckig weigert es zu lernen.
Aber privat werde ich mir das alles jetzt einmal aneignen, um mir
1.) Das Wissen verfügbar zu machen
2.) Den Nutzen verfügbar zu machen
3.) Beurteilen zu können, was dabei gut und schlecht ist,
4.) Ein und dasselbe Projekt einmal als Standard-Projekt mit normalen Design-Patterns und einmal mit dem EBC-Ansatz.
Hab gerade ein paar der Blogeinträge mit Begeisterung gelesen. Das mit dem Valve gefiel mir gut (Tracing einschalten, ausschalten).
Vielleicht werde ich auch die Leute hier ein wenig "ärgern" indem ich diesen Ansatz im nächsten Projekte verwende. ;-)
Es scheint ja nicht wirklich schwierig zu sein.
Also DAS ist echt genial!
Das ist wirklich flexibler, als meine Micro-Pattern-Trilogie!
Ich glaube, ich konvertiere! (Zumindest probiere ich die EBC erstmal aus.)
mfg
Christian
Mir ist vorhin beim Autofahren ein "Aha!"-Erlebnis gekommen!
Meine "Micro-Pattern-Trilogie" ist gar nicht so unflexibel. Das heißt, ich kann damit im Grunde das Selbe erreichen, wie Ralf mit den Event-Based-Components hier gezeigt hat. Und das ist nicht viel aufwändiger.
Ich hatte nur beim gedanklichen Durchspielen eine Denkblockade.
Dazu werde ich noch einen Beitrag in meinem Blog hinterlassen.
Die MPT hänge ich doch noch nicht an den Nagel. *g*
Kommentar veröffentlichen
Hinweis: Nur ein Mitglied dieses Blogs kann Kommentare posten.