Follow my new blog

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.

Montag, 22. Februar 2010

Steckspiele - Event-Based Components verbinden

Wie ich hier erklärt habe, liegen mir die “traditionellen” Komponenten – bei aller Liebe – doch auch ein wenig im Magen. Sie passen noch nicht ganz in meine Vision von wirklich “zusammensteckbaren” Software-Bausteinen. Von Event-Based Components (EBC) erwarte ich mir nun einen Fingerzeig, wie es besser werden kann.

Das Grundmuster für Ihre Implementierung ist denkbar einfach:

  • EBC kommunizieren nur über Nachrichten. Anders als bei der Event-Driven Architecture (EDA) ist diese Kommunikation allerdings (zunächst) synchron. Am Programmablauf ändert sich durch EBC also erst einmal nichts.
  • EBC erhalten Aufforderungen zu einer Dienstleistungen über Input-Nachrichten; sie erteilen Aufforderungen zu Dienstleistungen über Output-Nachrichten.
  • Die Übersetzung dieses Nachrichtenflusses in Code ist schematisch:
    • Nachrichten werden durch eine je eigene Klasse repräsentiert
    • Input-Nachrichten werden von Methoden ohne Rückgabewert verarbeitet
    • Output-Nachrichten werden über Delegaten versandt

Eine Komponente mit einer Input- und einer Output-Nachricht

image

hat damit einen Kontrakt wie folgt:

interface IEventBasedComponent

{

    void ProcessIncomingCommand(IncomingCommand cmd);

    event Action<OutgoingCommand> OnOutgoingCommand;

}

Das ist quasi alles, was ich derzeit zum formalen Aufbau von EBC sagen kann. Der Rest sind Folgerungen.

Symmetrische unidirektionale Kommunikation

Zunächst ist z.B. zu bemerken, dass der Nachrichtenfluss immer unidirektional ist. Input und Output fließen immer von der Quelle zum Ziel; im Falle von Aufforderungen bedeutet das, vom Client zum Service.

Nachrichten “haben” keine Resultate wie Methodenaufrufe. Es kann nichts automatisch zurück fließen. Die Kommunikation zwischen EBC ist grundsätzlich symmetrisch. Client und Service unterscheiden sich nicht. Für eine Aufforderung ist der Client die Quelle einer Nachricht an das Ziel Service. Die Aufforderung ist der Output des Clients und der Input des Service. Und falls der Service ein Resultat an den Client zurück geben soll, dann schickt er ihm ebenfalls eine Nachricht, die sein Output und der Input der Clients ist.

Die Begriffe Client und Service, so natürlich sie in der Welt der “traditionellen” Komponenten sind, bekommen in der Welt der EBC also einen neuen Stellenwert. Früher war die Funktionseinheit, die einen Methodenaufruf absetzt, automatisch der Client. Sie fordert damit eine Dienstleistung von einer Funktionseinheit, von der sie abhängt. Client und Service stehen in einem asymmetrischen Verhältnis. Argumente in einem Methodenaufruf sind etwas anderes als der Rückgabewert.

Zwischen EBC fließen jedoch nur Nachrichten. Das fundamentale Verhältnis zweier Funktionseinheiten ist nicht mehr das zwischen Client und Service, sondern das von Quelle und Ziel. Ob die Quelle ein Client und das Ziel ein Service sind, ist nicht mehr eine Frage der Syntax, sondern der Semantik.

Unabhängige Komponenten

Aus der symmetrischen unidirektionalen Kommunikation mit der obigen Codierung ergibt sich, dass Event-Based Components unabhängig von einander sind. Das bedeutet, EBC sind wahrhaft “composable”. Wir können sie ohne Eingriff zusammenstecken. Wo keine Services mehr aufgerufen werden, da gibt es einfach keine Abhängigkeit mehr. Output-Nachrichten fließen einfach nur aus einer EBC hinaus. Ob und von wem sie empfangen werden… das weiß die Komponente nicht.

Wie das geht, zeigt eine erste Version der Interaktion zwischen zwei Komponenten, die ich schon im ersten EBC-Blogartikel benutzt habe. Das Szenario ist unvollständig, weil der Codegenerator fehlt, aber das möchte ich im Moment vernachlässigen:

image

Hervorhebenswert ist dabei, dass CompilerCoordinator und ParserWorker in einer Client-Service-Beziehung stehen, Anfrage und Antwort jedoch auf getrennten “Kanälen” fließen. Formal gibt es keinen Unterschied zwischen der Aufforderung zum Parsen und deren Resultat. Die Nachrichten fließen nur in die entgegengesetzte Richtung.

Auf den Input “Compile” reagiert der CompilationCoordinator mit dem Output “Parse”; der ist Input für den ParserWorker, der daraus “ASTGenerated” als Output erzeugt; der wiederum für den CompilationCoordinator Input ist und über einen hier nicht sichtbaren weiteren Schritt zum Output “CompilationResult” führt.

Die Kontrakte sehen dafür so aus:

compiler.contract.dll:

interface ICompilerCoordinator

{

    void ProcessCompilationRequest(CompilationRequest request);

    event Action<CompilationResult> OnCompilationResult;

    event Action<ParseRequest> OnParse;

    void ProcessParseResult(ParseResult result);

}

parser.contract.dll:

interface IParser

{

    void ProcessParseRequest(ParseRequest request);

    event Action<ParseResult> OnParseResult;

}

Zusätzlich gibt es noch einen Kontrakt für die Nachrichten, d.h. die gemeinsame Sprache der Komponenten:

message.contract.dll:

class CompilationRequest

{

    public string Source;

}

class CompilationResult

{

    public Func<double, double> Function;

}

class ParseRequest

{

    public string Source;

}

class ParseResult

{

    public ASTNode Root;

}

Die Interfaces erscheinen schon ein wenig anders als von den “traditionellen” Komponenten gewohnt – aber der wesentliche Unterschied zeigt sich erst bei Betrachtung der Abhängigkeiten zwischen Komponenten und Kontrakten.

Klassisch sehen die so aus:

image

Die Compiler-Implementation ist von zwei Interfaces abhängig: einem, das sie exportiert, und einem, das sie importiert. Je mehr Abhängigkeiten eine Komponente hat, desto mehr Kontrakte muss sie also referenzieren.

Dem steht nun die EBC-Variante entgegen:

image

Zwei Unterschiede fallen auf: Es gibt einen Kontrakt für die Nachrichten, der in diesem Szenario “traditionell” noch nicht nötig war. Viel wichtiger ist jedoch, dass jede Komponentenimplementation nur noch abhängig ist von einem Kontrakt. Das bedeutet, jede Komponente ist nur noch durch einen Kontrakt spezifiziert und nicht durch 1+n. Damit ist schon mal mein 4. Problem mit der “traditionellen” Komponentenorientierung gelöst. (Insgesamt fünf Probleme oder “Bauschmerzen” hatte ich im ersten EBC-Blogartikel genannt.) EBC-Komponenten lassen sich kompakter spezifizieren.

Aber nicht nur ist mein Problem 4 gelöst. Auch Problem 1 hat sich in Luft aufgelöst. Ich hatte bei aller Entkopplung von “traditionellen” Komponenten durch die separaten Kontrakte immer noch Bauchschmerzen, weil Clients von Services abhängig waren. Das ist bei EBC-Komponenten nun nicht mehr der Fall. Auch wenn EBC-Komponenten zusammengesteckt werden können und müssen, sind sie nicht mehr abhängig. Sie produzieren Output und erwarten Input. Das ist aus meiner Sicht etwas anderes; vielleicht nur subtil, aber immerhin.

Jetzt zur Frage, wie EBC-Komponenten “zusammen spielen”? Einen Container gibt es dafür bisher nicht, wenn ich es recht sehe. Aber es ist auch nicht schwer, EBC-Komponenten zusammen zu stecken. Sehen Sie selbst:

// Build

ICompilerCoordinator cc = new CompilerCoordinator();

IParser p = new Parser();

// Bind

cc.OnParse += p.ProcessParseRequest;

p.OnParseResult += cc.ProcessParseResult;

cc.OnCompilationResult += cr => Console.WriteLine(cr.Function(2));

// Run

cc.ProcessCompilationRequest(new CompilationRequest {Source = "2*x"});

Am besten instanziieren Sie zuerst alle Komponentenimplementationen (Build-Phase). Das ist im Gegensatz zur “traditionellen” Komponentenorientierung möglich, da es ja keine Abhängigkeiten mehr gibt, die über Konstruktoren injiziert werden müssten. (Ja, ich weiß, es ginge auch “traditionell” anders über Property-Injection, aber in der Praxis kommt die nicht sehr häufig vor. Sie fühlt sich im Vergleich zur ctor-Injection nicht so natürlich an.)

Wenn alle Komponenten instanziert sind, setzen Sie sie zum größeren Ganzen zusammen (Bind-Phase). Dazu registrieren Sie Input-Verarbeitungsfunktionen als Handler für Output-Events. Beispiel: Der CompilerCoordinator definiert mit dem OnParse-Event den Output ParseRequest. Auf dem registriert die Bindungsphase die Input-Methode ProcessParserequest() der Parser-Komponente. Damit sind CompilerCoordinator und Parser in eine Richtung zusammen gesteckt.

Dass der ParseRequest eine Antwort erwartet, sieht man ihm nicht an. Folglich muss für das ParseResult eine ebensolche Verbindung in umgekehrter Richtung hergestellt werden. EBC-Kommunikation ist eben wahrhaft symmetrisch.

Die Bindung von EBC-Komponenten ist also denkbar simpel. Input-Handler werden auf Output-Events registriert. Fertig. Sie müssen nur die Datenflüsse zwischen EBC-Komponenten identifizieren und wissen sofort, wie Sie sie umsetzen.

Dass das Nachricht für Nachricht in Handler-Event-Paaren geschieht, finde ich nicht schlimm. Im Gegenteil! Verbindungen zwischen Funktionseinheiten lassen sich dadurch viel detaillierter steuern. Sie müssen sich beim Entwurf einer Komponente keine Gedanken machen, welche andere Komponenten Outputs weiter verarbeiten oder Inputs liefern.

So löst sich für mich auch mein Problem Nr. 3 mit der “traditionellen” Komponentenorientierung in Luft auf. Ob Inputs und Output von einer anderen Komponente bedient werden oder von vielen… Das ist egal. Es kann sich deshalb auch während der Entwicklungszeit problemlos ändern. Betroffen sind dann allein die Build-Bind-Phasen, aber nicht die Komponentenimplementationen. Die haben ja keine Abhängigkeiten zu anderen Kontrakten.

Damit ist auch dem Prinzip Genüge getan, dass Komplexes wenige Abhängigkeiten haben sollte und Einfaches durchaus viele Abhängigkeiten haben kann. Denn komplex sind Komponentenimplementationen, die durch EBC quasi abhängigkeitsfrei sind. Build und Bind hingegen sind trivial und dürfen deshalb viele Abhängigkeiten haben.

Mit EBC-Komponenten sind wir also bei Platinen und Bauteilen angekommen. Build+Bind entsprechen einer elektronischen Platine, die Bauteile verbindet. Welche Bauteile das sind, hängt vom Zweck eines Gerätes ab. Dem muss sich die Platine anpassen und die Bauteilauswahl. Die Bauteile jedoch selbst, die bleiben unverändert. Ich nehme den Begriff der Wiederverwendbarkeit nur ungern in den Mund, aber hier passt er mal. Das Potenzial für Wiederverwendbarkeit scheint mir bei EBC-Komponenten dank ihrer Entkopplung und zentralen Definition mittels nur eines Kontraktes höher als bei “traditionellen” Komponenten.

Geschachtelte Komponenten

Hm… vielleicht ist die Analogie mit den elektronischen Bauteilen passender als zunächst gedacht. Ein Software-Bauteil ist dann immer eines, für das es einen EBC-Kontrakt mit einer zugehörigen Implementation gibt. Ob diese Implementation selbst Geschäftslogik ist oder nur Geschäftslogik-Bauteile verdrahtet… das ist egal. Im letzteren Fall wäre sie eine Platine, also ein Aggregat von Bauteilen, das wieder ein Bauteil auf höherer Ebene darstellt.

Software-Platinen könnten hybride Komponenten sein: Einerseits implementieren Sie einen EBC-Kontrakt, andererseits werden sie initialisiert wir “traditionelle” Komponenten. Hier eine “Compiler-Platine” als Beispiel. Deren Kontrakt könnte so aussehen:

interface ICompiler

{

    void ProcessCompilationRequest(CompilationRequest request);

    event Action<CompilationResult> OnCompilationResult;

}

Dass es da keinen Unterschied zum CompilerCoordinator gibt, ist völlig ok. Wenn der CompilerCoordinator die ganze Interaktion des Compilers mit der Umwelt verkörpert, dann werden dessen “Signalleitungen” zu Inputs- und Outputs der umfassenden Platine.

Die ist selbst eine EBC-Komponente qua Kontrakt – ist jedoch abhängig von ihren Bauteilen. Und die injiziert ihr ein DI Container wie üblich:

class Compiler : ICompiler

{

    private ICompilerCoordinator cc;

    public Compiler(ICompilerCoordinator cc, IParser p)

    {

        this.cc = cc;

        // Bind

        cc.OnParse += p.ProcessParseRequest;

        p.OnParseResult += cc.ProcessParseResult;

        // Output “von innen” weiterleiten zur “Platine”

        cc.OnCompilationResult += cr => this.OnCompilationResult(cr);

    }

   
    public void ProcessCompilationRequest(CompilationRequest request)

    {

        // Input der “Platine” weiterleiten “nach innen”

        this.cc.ProcessCompilationRequest(request);

    }


    public
event Action<CompilationResult> OnCompilationResult;

}

EBC und “traditionelle” Komponenten sind also kein Widerspruch, sondern ergänzen sich. “Software-Platinen” sind hybride Komponenten, die nur den Zweck haben, EBC zu aggregieren. Sie dürfen Abhängigkeiten haben, weil sie so simpel sind. Nach außen verhalten Sie sich aber wie EBC.

Im folgenden Bild sind die schwarzen Kästen “atomare” EBC; die offenen Kästen sind “Platinen” – die jedoch genauso wie die schwarzen Kästen Input- und Output-Pins haben. Formal kann man beide also nicht auseinander halten:

image

Ob die Implementation eines EBC-Kontrakts in einer “atomaren” EBC besteht oder in einer hybriden, das muss auch einerlei sein, denn das kann sich jederzeit ändern, weil ein “Software-Bauteil” keine sichtbaren Abhängigkeiten hat.

Mir scheint solche Schachtelung von Funktionsbausteinen mit EBC einfacher als rein mit “traditionellen” Komponenten. Deshalb lösen EBC auch mein Problem Nr. 5. Die Schachtelungsregeln sind ganz einfach:

  • Eine EBC ist ein “Software-Bauteil”.
  • Wo zwei oder mehr “Software-Bauteile” mit einander verdrahtet werden, entsteht ein neues “Software-Bauteil”. Dessen Kontrakt besteht aus den in ihm unverdrahtet gebliebenen Input- und Output-Pins seiner Konstituenten.

Bin ich damit bei einem neuen Begriff angekommen, dem “Software-Bauteil”? Gibt es EBC und EBP, also Event-Based Parts? Ist eine Component ein bestimmte Art von Part. Hm… darüber muss ich mal nachdenken. Aber als nächstes will ich erstmal beschreiben, wie die Kommunikation zwischen Parts einfacher werden kann.

PS: Von meinen 5 Problemen sind bisher 4 gelöst. Was ist aber mit Problem Nr. 2? Sind EBC “besser” als “traditionelle” Komponenten, weil in sie nichts mehr injiziert wird? Oder wird noch injiziert, nur anders?

Eine Ctor-Injection oder auch eine Property-Injection eines ganzen Interface scheint mir schwergewichtiger zu sein als die Injektion einzelner Eventhandler. Ganz einfach, weil die übliche DI sich auf ganze Interfaces bezieht. Da wird immer eine Menge auf einmal in eine Komponente hinein gesteckt. Wie schlank ist dem gegenüber die Registrierung einiger Eventhandler.

Deutlich wird das für mich, wenn ich daran denke, was passiert, wenn sich die Zusammensetzung eines importierten Kontrakts ändert. Wird eine Operation herausgelöst in eine andere “traditionelle” Komponente, dann müssen alle abhängigen ihre Injektionsstellen nachführen.

EBC merken davon nichts. Wird ein Output von einem anderen EBP verarbeitet, ändert sich etwas an der Platinen, die beide verdrahtet, aber nichts an der Implementation dort, wo der Output heraus kommt.

Für mich fühlt sich das an wie eine Lösung für mein Problem Nr. 2.

Ha! Wer hätte das gedacht. So entpuppen sich EBC als den “traditionellen” Komponenten überlegen. Die werden, wie oben bei den hybriden Komponenten gezeigt, allerdings nicht ersetzt, sondern bekommen einen neuen Platz zugewiesen.

Wer abhängig ist, der darf nur einfaches tun, z.B. zusammenstecken. Wer unabhängig ist, der darf kompliziertes tun, z.B. Geschäftslogik implementieren. Die Unterscheidung zwischen EBC und “traditionellen” Komponenten scheint mir damit ganz in der Linie des Separation-of-Concerns-Prinzips zu liegen.