Follow my new blog

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.

Sonntag, 21. Februar 2010

Event-Based Components – Der nächste Schritt der Komponentenorientierung?

Wie wäre es eigentlich, wenn wir Komponenten zwar synchron, aber nachrichtenorientiert koppeln würden? Wie wäre es, wenn EDA – Event-Driven Architecture – nicht nur eine Sache großer Anwendungen wäre, sondern in Form von Event-Based Components (EBC) auch kleine Applikationen evolvierbarer gestalten helfen würde?

Über die Vorteile von Komponentenorientierung an sich möchte ich mich hier nicht schon wieder auslassen ;-) Komponenten als binäre Codeeinheiten mit separatem Kontrakt sind für mich schlicht die Basis jeder bewussten Anwendungsarchitektur.

Wie solche Komponenten aber definiert und dann in Code gegossen werden, darüber lässt sich immer wieder nachdenken. Bisher habe ich den folgenden Ansatz vertreten:

  • Die Architektur einer Software resultiert in einem Komponentenabhängigkeitsdiagramm.
  • Die Implementation der Komponenten verteilt sich auf zwei Assemblies: eine für den Kontrakt, eine für die Funktionalität.
  • Wenn Komponenten andere brauchen, d.h. von ihnen abhängig sind, dann werden diese Komponenten der abhängigen von einem DI Container injiziert.

Komponentenorientierung “traditionell”

Hier eine ganz einfache Architektur mit drei Komponenten für ein Szenario, dass Ihnen bekannt vorkommen sollte ;-)

image

Daraus ergäben sich die Assemblies:

  • compiler.contract.dll
  • compiler.dll
  • parser.contract.dll
  • parser.dll
  • codegenerator.contract.dll
  • codegenerator.dll

compiler.dll würde compiler.contract.dll referenzieren, weil sie deren Kontrakt exportiert, d.h. implementiert. Und compiler.dll würde parser.contract.dll sowie codegenerator.contract.dll referenzieren, weil sie deren Kontrakte importiert. Zur Laufzeit braucht compiler.dll dann natürlich Implementationen dieser Kontrakte. Kennen tut sie deshalb parser.dll und codegenerator.dll nicht. Dependency Injection macht es möglich.

Und wie sehen die Kontrakte aus? Das hängt von den Diensten der Komponenten ab. Üblicherweise wird jede Komponente mindestens durch ein Interface für seine “Hauptdienstleistung” definiert.

compiler.contract.dll:

interface ICompiler

{

    Func<double, double> Compile(string formel);

}

parser.contract.dll:

interface IParser

{

    ASTNode Parse(string formal);

}

codegenerator.contract.dll:

interface ICodeGenerator

{

    Func<double, double> Translate(ASTNode program);

}

Und wo ist der ominöse ASTNode definiert? Da zwei Komponenten ihn brauchen, die nicht von einander abhängen, brauchen wir noch eine weitere Assembly:

compiler.datenmodell.contract.dll:

class ASTNode

{

   

}

Diese reine Kontraktassembly wird von allen anderen referenziert. Sie enthält einen allen gemeinsame Vorstellung davon, wie Code im Compiler repräsentiert werden sollte: als abstrakter Syntaxbaum.

Die Implementationen der Komponenten ist wenig spannend. Nur den Compiler möchte ich hervorheben. Er ist ja abhängig von den anderen beiden Komponenten und muss daher Instanzen von ihnen zur Laufzeit bekannt gemacht bekommen:

class Compiler : ICompiler

{

    private readonly IParser parser;

    private readonly ICodeGenerator codegenerator;

    public Compiler(IParser parser, ICodeGenerator codegenerator)

    {

        this.parser = parser;

        this.codegenerator = codegenerator;

    }

    public Func<double, double> Compile(string formel)

    {

        var program = this.parser.Parse(formel);

        return this.codegenerator.Translate(program);

    }

}

Diese Bekanntmachung ist kein Hexenwerk. Mit einem DI Container ist das ganz einfach. Der übernimmt die Befüllung der Ctor-Parameter bei Instanzierung eines Compiler-Objektes. Hier ein Beispiel mit Microsoft Unity:

IUnityContainer uc = new UnityContainer();

uc.RegisterType<IParser, Parser>();

uc.RegisterType<ICodeGenerator, CodeGenerator>();

uc.RegisterType<ICompiler, Compiler>();

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

var f = c.Compile("2*x");

Soweit meine bisherige Vorstellung von Komponentenorientierung. Das funktioniert alles wunderbar. Ist erprobt. Läuft. Alles kein Problem.

Und doch… irgendwie bin ich nicht so ganz zufrieden damit. Mich sticht der Zweifel, ob das schon der Weisheit letzter Schluss ist in Bezug auf “Composability”. Kann man Komponenten, also die architekturellen Grundbausteine von Software, in dieser Weise schon optimal “zusammenstecken”?

Kritik der “traditionellen” Komponentenorientierung

Wenn Komponenten Bausteine sein sollen wie Lego-Bausteine oder elektronische Bauteile, dann, so glaube ich, widerspricht dem, wie wir mit ihren Abhängigkeiten umgehen.

Erstens dürfen Komponenten beim “traditionellen” Ansatz überhaupt Abhängigkeiten haben. Das scheint mir nicht ganz passend in Bezug auf den Baustein-Begriff. Ein Lego-Baustein braucht keine anderen. Er passt mit anderen zusammen, aber er braucht sie nicht. Dasselbe gilt für einen Transistor oder einen Widerstand oder auch einen Prozessor. Keines dieser Bauteile braucht ein anderes zum Funktionieren. Es braucht Signale (Input) im Rahmen einer Spezifikation, aber woher diese Signale kommen und wohin der eigene Output geht… das ist den Bauteilen egal. Davon wissen sie nichts. Dafür ist die Platine zuständig, in der sie stecken.

Zweitens injizieren wir die Abhängigkeiten in Komponenteninstanzen. Wir verändern die Komponenten also zur Laufzeit, um ihre dynamischen Abhängigkeiten zu befriedigen. Das fühlt sich zumindest irgendwie merkwürdig im Vergleich zu realweltlichen Bausteinen an. Und es kann für Testzwecke relativ aufwändig sein, wenn dafür Attrappen gebaut werden müssen.

Drittens sind die Abhängigkeiten gewöhntlich definiert in Form von Interfaces. Es geht also immer um Bündel von Operationen. Eine abhängige Komponente läuft damit aber Gefahr, Zugriff auf Operationen zu bekommen, die sie nichts angehen. Im obigen Beispiel wird das nicht deutlich, aber sobald Servicekomponenten mehreren Clientkomponenten dienen, wachsen ihre Kontrakte schnell über das hinaus, was einer dieser Clients von Ihnen braucht.

Viertens ist die Spezifikation einer Komponente nicht sehr kompakt, wenn ich dafür mehrere Kontrakte anschauen muss. Sie besteht ja aus dem exportierten und allen importierten Kontrakten. Wenn ich wissen will, wie eine Komponente zu implementieren ist, dann sind 1+n Kontrakte zu konsultieren. Es gibt keinen einen Ort, an dem kompakt beschrieben ist, wie eine Komponente mit ihrer Umwelt interagiert.

Fünftens tue ich mich immer noch schwer mit der Schachtelung von Komponenten. Im Beispiel oben habe ich zwar drei hübsche Komponenten, aber eigentlich würde ich sie anders bezeichnen und in eine umfassendere einschachteln wollen:

image

Erst diese umfassende Codeeinheit wäre der Compiler. Die bisherigen sind nur seine Bausteine. Mit der bisherigen Komponentenorientierung finde ich das zu planen aber nicht so einfach. Wie soll die umfassende Codeeinheit auch eine Komponente sein, wenn es ihre Konstituenten auch sind?

Das sind für mich genügend Gründe, weiter darüber nachzudenken, wie Komponenten noch besser gebaut werden können, so dass Software wahrhaft aus Bausteinen besteht.

Ein Ansatz dafür, den ich gerade spannend finde, ist die konsequente Ereignisorientierung.

Komponentenkommunikation über Ereignisse

Fünf Gründe, über die “traditionelle” Komponentenorientierung nachzudenken. Aber geht es anders irgendwie auch fünf Mal besser? Ich glaube, schon. Mit der Ereignisorientierung lösen sich manche Probleme in Luft auf und anderes wird klarer, wie mir scheint.

image Auf geht´s… Was bedeutet Ereignisorientierung für Komponenten? Zum Thema gibt es ein interessantes Buch: Event-based Programming von Ted Faison. Das hat mich auf die Spur von Event-Based Components gebracht. Allerdings weicht meine Vorstellungen in einigen Punkte von der des Buches ab. Mir ist seine Darstellung gerade bei der Praxis ein wenig zu allgemein. Aber die Grundgedanken darin finde ich sehr spannend… Hier deshalb meine Version ihrer Implementation.

Ich fange mal mit einer etwas anderen Notation an als der bisherigen. Die ist den Wire-Diagrams (Signaldiagramme) des Buches angelehnt. Hier zum warm werden eine ganz simle Event-Based Component (EBC), die als Dienstleistung die Verarbeitung eines Kommandos anbietet. Kommando bedeutet dabei im Sinne der Command-Query Separation, dass kein Resultat an den Aufrufer geliefert wird.

image

Ein Rechteck statt eines Kreises bzw. einer Ellipse ist nur ein oberflächlicher Unterschied zwischen den Diagrammen. Die wahre Andersartigkeit steckt in der Implementation. Hier der Kontrakt für die Komponente:

interface IEventBasedComponent

{

    void ProcessIncomingCommand(IncomingCommand cmd);

}

Wie bei der “traditionellen” Komponentenorientierung ist der Kontrakt einer EBC ein Interface. Für jede eingehende Nachricht, die eine Komponente "versteht”, gibt es darin eine Methode mit einem Parameter. Der hat einen für jede Nachricht spezifischen Typ.

Ob die Methode dann wie oben den Typ ihrer Nachricht im Namen widerspiegeln sollte oder nicht, weiß ich noch nicht so recht. Im Augenblick ist das mal meine Konvention inklusive eines Prefixes wie “Process”/”Execute” (für Kommandos) oder “Fulfill”/“Inquire” (für Queries).

EBC-Methoden für eingehende Nachrichten haben damit keine normale Signatur mit vielen Parametern oder einem Return-Wert. Es sind immer nur void-Methoden mit nur einem Parameter.

Jetzt zu ausgehenden Nachrichten oder besser zum Output. Denn um nichts anderes handelt es sich, auch wenn solche Nachrichten eine Antwort erwarten, wenn sie eine Komponente verlassen.

image

Hier unterscheiden sich Event-Based Components nun deutlich von den “traditionellen”. Output-Nachrichten werden ausschließlich über Events verschickt. Der Kontrakt sieht dafür dann so aus:

interface IEventBasedComponent

{

    event Action<OutgoingCommand> OnOutgoingCommand;

}

Das ist der Trick an der ganzen Sache mit der Ereignisorientierung: Nachrichten an andere Komponenten werden als Events verschickt. Wieder haben die Methoden nur einen Parameter und keinen Rückgabewert.

Bei der Benennung der Delegaten folge ich wieder einem Muster. Als Name verwende ich wieder den Nachrichtentyp und setze einen Präfix davor, z.B. “On” oder “Issue” (für Kommandos) oder “Request” (für Queries).

Hört sich irgendwie nicht so spektakulär an, oder? Ich glaube aber nach erster Evaluation, dass die Beschränkung der Kommunikation auf synchrone Input- und Output-Nachrichten in dieser Weise grundsätzliche und positive Folgen hat. Doch davon ein andermal…

Freitag, 19. Februar 2010

Einfacher im Gleichschritt – Dienstaufrufe mit der CCR synchronisieren

In der dotnetpro 3/2010 gehen Tobias Richling et al. in einem Artikel der Frage nach, wie asynchrone WCF-Dienstaufrufe synchronisiert werden können. Sie überlegen also, wie ein Client, der Service S1 und Service S2 asynchron (!) aufruft, es so einrichten kann, dass er erst weiter macht, wenn beide (!) Services ein Resultat geliefert haben.

Bei synchronen Aufrufen stellt sich diese Frage nicht. Da geht es ohnehin erst weiter im Client, wenn beide Aufrufe zurückgekehrt sind.  Hier das Beispiel dazu aus dem Artikel:

image

Im Client geht es erst nach der letzten Zeile weiter, wenn sowohl GetArticleList() wie auch GetSpecialOfferArticle() synchron aufgerufen wurden.

Symptomkur

Was aber, wenn beide Aufrufe asynchron sind? Bei Silverlight ist das gar nicht anders möglich. Und bei anderen Client-Plattformen sollten Sie auch darüber nachdenken, denn asynchrone Aufrufe bringen Performancevorteile. Sie laufen ja parallel, egal ob der Client mit einem oder mehreren Prozessorkernen ausgestattet ist.

Im Artikel sieht ein asynchroner Aufruf mit WCF so aus:

image

Nicht ganz trivial, oder? Und davon gibt es zwei – die dann eben, wenn nacheinander ausgeführt, zum Problem führen: Der Client muss irgendwie dafür sorgen, dass er auf die Ergebnisse von proxy.GetArticleListAsync() und proxy.GetSpecialOfferArticleAsync() wartet, bevor er weiter macht.

Wie das gehen kann, dazu macht sich der Artikel Gedanken und stellt am Ende eine allgemeine Lösung vor. Mit der sieht das Warten dann so aus:

image

Man registriert einen Eventhandler (AllComplete), der gerufen wird, wenn schließlich beide Dienstaufrufe zurückgekehrt sind, registriert die Dienstaufrufe (AddServiceCall()), die getätigt werden sollen und startet sie schließlich (ExecuteAll()). Das alles macht möglich ein sog. ServiceCallSynchronizer.

Das sieht auf den ersten Blick doch ordentlich aus. Auf den zweiten schleicht sich bei mir aber zumindest kleines Unbehagen ein, weil die Dienstaufrufe nicht typsicher registriert werden. Die Methodennamen sind als Zeichenketten anzugeben. Ob es eine Methode gibt, stellt sich also erst zur Laufzeit heraus. Ebenso gibt es keine Argumentprüfung zur Compilezeit, weil ja AddServiceCall() nicht weißt, ob und welche Parameter eine Dienstmethode hat. Das ist beides nicht schön.

Wer mit WCF arbeiten muss, mag den Autoren aber dennoch danken. Sie lösen mit Liebe ein Problem, das ansonsten ein Projekt ins Stocken bringen kann. Immerhin umfasst der allgemeine Aufrufsynchronisierer einige Dutzend Zeilen, über die Sie sich nun nicht mehr den Kopf zerbrechen müssen:

image

Wer die Lösung im Detail sehen möchte, der lese den Artikel.

Das Übel an der Wurzel packen

Ich habe nichts gegen WCF – aber dieses Beispiel zeigt mir wieder, dass ich mit WCF nicht direkt arbeiten möchte in meinem Anwendungscode. Ich halte die im Artikel vorgestellte Lösung für eine Symptomkur. Das Symptom: WCF bietet keinen Synchronisationsmechanismus für asynchrone Aufrufe.

Und was ist das Grundproblem, wenn das nur ein Symptom ist? WCF ist das Grundproblem. Denn WCF ist ein Frameworkbolide, der immer noch in der synchronen Kommunikation verwurzelt ist. Asynchronizität geht zwar auch irgendwie, ist aber nicht der Fokus. Dabei ist Asynchronizität – zumindest aus meiner Sicht – eine unverbrüchliche Bedingung für die Kommunikation in verteilten Anwendungen.

Aber ich will hier nicht philosophisch werden ;-) Lieber zeige ich einfach, wie aus meiner Sicht das Übel an der Wurzel gepackt werden könnte. WCF will ich dabei gar nicht ersetzen. WCF soll nur für mich als Anwendungsprogrammierer unsichtbar werden.

Hier meine Lösung:

image

That´s it.

Nur eine Kleinigkeit fehlt darin, die ich aber nur ausgelagert habe, um das Abstraktionsniveau einheitlich zu halten. Doch das ist auch nur eine simple Erweiterungsmethode:

image

Sieht das übersichtlich aus? Ist das alles typsicher? War das “out of the box” möglich? Drei Mal Ja.

Übersichtlich, typsicher und “out of the box” ist das alles, weil ich eben nicht WCF benutze, sondern ein Kommunikationsframework, das konsequent auf Asynchronizität setzt. Das ist der Xcoordination Application Space (AppSpace), der als Open Source Komponente bei CodePlex liegt und die Verteilung von Anwendungen erlaubt, die mit Microsofts Concurrency Coordination Runtime (CCR) arbeiten.

Wie funktioniert nun mein Code? Dazu müssen Sie verstehen, wie Dienste im AppSpace aufgerufen werden. Das geschieht immer über sog. Ports. Das sind typisierte Nachrichtenkanäle, die die CCR zur Verfügung stellt.

Dienste halten Sie also nicht in Form von Methoden in der Hand, sondern als Ports. Sie können AppSpace-Dienste nur indirekt anstoßen vermittels von Nachrichten. AppSpace-Anwendungen haben also immer eine Event-Driven Architecture (EDA), denn Dienste registrieren Eventhandler auf ihren Ports, um an die Nachrichten ihrer Clients zu kommen.

Einen Dienst einzurichten, ist mit dem AppSpace sehr einfach. Hier die Dienstimplementation:

public class MyArticleService : PArticleService

{

    [XcoConcurrent]

    internal void ProcessGetArticleList(GetArticleList query)

    {

        …

    }

 

    [XcoConcurrent]

    internal void ProcessGetSpecialOfferArticle(GetSpecialOfferArticle query)

    {

        …

    }

}

Und hier der Code, mit dem der Dienst gehostet wird, so dass Clients ihn aufrufen können:

using(var server = new XcoAppSpace("wcf.port=12345"))

{

    server.RunWorker<PArticleService, MyArticleService>("TheArticleService");

    …

Das ist nicht sehr aufwändig, oder? Und ich muss gar nicht auf die Segnungen von WCF verzichten, wie der Konfigurationsstring des AppSpace zeigt. Nur sind mir die ganzen lästigen WCF-Details einerlei. Ich kann mich auf das Wesentliche konzentrieren: Geschäftslogik.

Und wo sind die ominösen CCR Ports? Die stecken im Kontrakt des Dienstes, denn natürlich hat der AppSpace auch sein ABC: Address, Binding, Contract. Die Adresse des Dienstes ist localhost:12345/TheArticleService. Das Binding ist Net.Tcp von WCF. Und der Contract sieht so aus:

[Serializable]

public class Article

{

    public string description;

}

 

public class PArticleService : PortSet<PArticleService.GetArticleList,

                                       PArticleService.GetSpecialOfferArticle>

{

    [Serializable]

    public class GetArticleList

    {

        public Port<Article[]> response;       

    }

 

    [Serializable]

    public class GetSpecialOfferArticle

    {

        public int forMonth;

        public int forYear;

 

        public Port<Article> response;

    }

}

Das sieht für Sie sicher ungewöhnlich aus. Aber im Grunde ist es nicht viel anders als bei WCF. Ein AppSpace-Dienst hat auch eine Schnittstelle. Die besteht aber nicht aus einem interface sondern aus einer Liste von CCR Ports (PArticleService). Auf jedem Port “lauscht” dann ein Eventhandler (s. Process…() Methoden in der Dienstimplementation).

Ports haben keine Signatur, sondern transportieren nur Nachrichten eines Typs. Deshalb müssen die üblichen Dienstmethodensignaturen in Nachrichtentypen übersetzt werden. Aus

Article GetSpecialOfferArticle(int forMonth, int forYear);

wird dann ein Port für GetSpecialOfferArticle-Nachrichten. Und jede Nachricht enthält die ursprünglichen Methodenparameter als Felder. Ein Resultat liefert die Dienstmethode schließlich via einem Antwort-Port (response).

Das ist anders als bei WCF und Webservices. Aber ich würde sagen, es ist nicht schwer zu verstehen. Vor allem ist es aber 1. symmetrisch und 2. weniger trugschlussbehaftet.

Die Symmetrie zwischen Client und Service ergibt sich daraus, dass die Kommunikation in beide Richtungen via Ports läuft. Dadurch werden z.B. Notifkationen ein Kinderspiel. Und Sie unterliegen weniger Trugschlüssen bzgl. der Kommunikation, weil Sie sofort sehen, dass sie verteilt/asynchron ist, dass sie damit mal länger dauern kann, dass der Dienst auch mal nicht antworten kann usw.

Der Client holt sich dann einen “Proxy” für einen entfernten AppSpace-Dienst mit ConnectWorker<T>(). Der bietet Ports entsprechend dem Kontrakt, an die der Client seine Nachrichten schickt.

Und jetzt der Trick mit der Synchronisation: Dafür muss ich keinen Aufwand treiben, weil die CCR einen Synchronisationsoperator bietet, ein Join. Mit ihm kann der Client ganz einfach darauf waren, dass Nachrichten in mehreren Ports angekommen sind. Im Beispiel sind das die Antwort-Ports, die der Client in die Nachrichten an den Dienst gesteckt hat.

Eine Anweisung (client.Join()) genügt daher, um den Client-Code, der mit den Ergebnissen beider Dienstaufrufe weiter arbeiten sollen, erst dann auszuführen, wenn auch beide Dienste geliefert haben. Egal, in welcher Reihenfolge sie fertig geworden sind.

Fazit

Ich kann mir nicht helfen, aber ich finde die AppSpace/CCR-Lösung verständlicher, kürzer und “ehrlicher”. Das liegt für mich daran, dass AppSpace/CCR für Asynchronizität gemacht sind. Wo WCF sich strecken muss, um seinem synchronen Erbe zu entwachsen, da sind AppSpace/CCR schon lange angekommen.

Asynchronen Code zu schreiben, ist ungewohnt. Aber mit ein wenig Übung werden Sie feststellen, dass schon in relativ einfachen verteilten Szenarien dadurch viele Vorteile entstehen. Gerade für den “kleinen Verteilungshunger zwischendurch”, d.h. dort, wo Sie bisher nicht an Verteilung zu denken gewagt haben, da bieten AppSpace/CCR Ihnen eine Plattform, die es Ihnen einfach macht, den Einstieg zu finden. Ob die Kommunikation dann “auf dem Draht” mit WCF (TCP, Named Pipes) oder MSMQ oder Jabber oder Named Pipes läuft, das ist Ihnen egal. Ihr Programmiermodell ist immer gleich. Probieren Sie es mal aus.