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.
Jedem Event sind also potenziell mehrere Event-Handler zugeordnet:
Und falls es mehrere Events vom selben Typ gibt, dann sind die Event-Handler mit all diesen Events verbunden:
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:
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:
- Sammle alle Output-Pins
- Sammle alle Input-Pins
- Für jeden Output-Pin…
- Finde alle Input-Pins, die zu seinem “qualifizierten Typ” (Typ + Pinname) passen
- Wenn es solche Pins gibt, dann registriere sie als Event-Handler…
- …ansonsten suche alle Pins, die nur zu seinem Typ passen
- Wenn es solche Pins gibt, dann registriere sie als Event-Handler…
- …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:
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:
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:
Probieren Sie den Binder mal aus. Dann diskutieren wir im Forum des Projektes, wie es damit weitergehen kann.