Follow my new blog

Freitag, 16. April 2010

Kanalisiert – Event-Based Components im Dialog

Die Pins von Event-Based Components (EBC) sind bisher ganz einfach: Output-Pins sind Delegaten, Input-Pins sind Methoden. Beide haben 1 Parameter und liefern kein Resultat. Damit können Sie alle Kommunikationswünsche erfüllen. Irgendwie. Manchmal ist es jedoch etwas umständlich. Die Simplizität und Regelmäßigkeit der Beschränkung auf nur eine Signatur hat das für mich jedoch bisher wett gemacht.

class Client
{
    public Action<string> pingEvent;
    …

class Service
{
    public void DumpText(string text)
    {
        Console.WriteLine("text received: {0}", text);
    }
    …

Ein Artikel über Axum in der dotnetpro hat mich nun allerdings nochmal nachdenken lassen. Ich bin weiterhin sicher, dass Pins keine beliebige Form haben sollten. Eine überschaubare Menge an Pin-“Formen” macht ein Tooling einfacher.  Andererseits wäre es schön, Kommunikationsmuster nicht immer wieder selbst mit speziellen Nachrichtentypen modellieren zu müssen.

Beispielhaft habe ich für eine Request/Response Kommunikation hier demonstriert und auch schon versucht, den Aufwand hinter einer Erweiterungsmethode zu verstecken. Für diese einmalige bi-direktionale Kommunikation ist das ok. Was aber, wenn zwei EBCs einen Plausch halten wollen? Ich skizziere das mal mit Standardtypen:

EBC A sagt… EBC X antwortet…
string  
  int
double  
  DateTime
bool  

Der Plausch beginnt, indem A an X einen string sendet. (Dass A nichts von X weiß, sondern einen string-Event feuert, lasse ich einmal außen vor. Auf einer Platine sind A und X mit einer Leiterbahn verbunden. Die Formulierung “sendet an” ist schlicht leichter zu verstehen.) Daraufhin antwortet X mit einem int-Wert. Den nimmt A auf und führt den Plausch mit einem double-Wert an X weiter. Den beantwortet X mit einem DateTime-Objekt und A beendet den Plausch mit einem bool-Wert.

So eine Konversation können Sie mit den bisherigen Pin-Standardformen implementieren, doch das ist umständlich. Der Gesprächsfluss ist in den Methoden bei A und X nur schwer zu erkennen. Probieren Sie es aus.

Dies und anderes wird aber einfacher, wenn Sie EBC-Pins nicht mehr “durch blankes Metall” und quasi direkt verbunden sehen, sondern durch eine sichtbare Leitung. Darauf hat mich der Axum-Artikel gebracht. Dort läuft die Kommunikation zwischen Axum Agenten nämlich durch Channels.

Mit diesem Konzept habe ich nun ein wenig in der EBC-Welt gespielt. Herausgekommen ist das Folgende.

EinwegKommunikation

Wenn ein EBC nur einen Event feuern will, ohne ein Ergebnis im weiteren Verlauf der Methode zu erwarten, dann ist das eine Einwegkommunikation. Die wird ganz natürlich durch einen Delegaten als Output-Pin beschrieben. Das kann auch so bleiben. Eine Modellierung mittels Channels sollte auch hier jedoch schon möglich sein. Die sähe z.B. so aus:

class Client
{
    public TriggerChannel noDataChannel;
    public OneWayChannel<string> pingChannel;
    …

Auf die Input-Pins haben Channels nur wenig Einfluss. Die werden weiterhin als Methoden implementiert:

class Service
{
    public void Reactor()
    {
        Console.WriteLine("reacting");
    }

    public void DumpText(string text)
    {
        Console.WriteLine("text received: {0}", text);
    }
    …

Bitte beachten Sie: Ich erlaube jetzt auch parameterlose Methoden! Das scheint mit durch Einführung der Channels eine angebrachte Lockerung der Regelmäßigkeit. Events ohne Nachrichten tauchen häufig in Architekturen auf. Für die einen Dummy-Nachrichtentyp einzusetzen, erhöht die Lesbarkeit dann nicht.

Verbunden werden Output- und Input-Pin mit Channel-Objekten:

Client c = new Client();
Service s = new Service();

c.noDataChannel = new TriggerChannel(s.Reactor);
c.pingChannel = new OneWayChannel<string>(s.DumpText);

Der OneWayChannel<> bringt hier zwar noch keinen Vorteil gegenüber den bisherigen Events. Dennoch scheint er mit angebracht, um alles, was bisher auch ohne Channels möglich war, konsequent mit Channel modellieren zu können.

Events feuert der Client nun allerdings nicht mehr so offensichtlich. Seine Output-Pins sind ja Channels und über die werden Nachrichten versandt:

this.noDataChannel.Trigger();
this.pingChannel.Send(“hello, world!”);

Für das grundsätzliche Prinzip der Kopplung von EBCs macht das keinen Unterschied. Alle Vorteile von EBCs gegenüber Injection-Based Components (IBC) bleiben erhalten. Und weitere kommen hinzu…

ZweiwegeKommunikation

Durch Channels auch Events ohne Parameter feuern zu können, ist nett. Noch netter ist es allerdings, eine saubere Abbildung für die Request/Response-Kommunikation zu haben. Der Vorschlag eines speziellen Request<,>-Nachrichtentyps, den ein Tooling natürlich auch erkennen könnte, war praktikabel – doch irgendwie hat er das saubere Paradigma “verdreckt”.

Die Kommunikation grundsätzlich über Channels laufen zu lassen und dann für Request/Response einen speziellen Channel zu haben, scheint mir sauberer. Aber sehen Sie selbst:

class Client
{
    public RequestResponseChannel<string, string> echo;
    …

Passende Input-Pins können verschiedene Formen haben:

class Service
{
    public string EchoFunc(string text)
    {
        return text.ToUpper();
    }

    public void EchoAction(string text, Action<string> reply)
    {
        reply(text.ToUpper());
    }
    …

Endlich können Sie in der Kommunikation zwischen Komponenten wieder Funktionen verwenden :-) Ist das nicht ein Gewinn. Die Regelmäßigkeit des Einsatzes von Channels erlaubt auch hier wieder etwas Öffnung für das Korsett der Input-Pin-Signaturen. Funktionen sind “serverseitig” die natürliche Form für Query-Prozessoren.

Alternativ können Sie aber auch Antworten über einen reply-Pin zurückreichen. Das bietet sich an, wenn für eine Anfrage mehr als ein Ergebnis erzeugt wird – und diese Pin-Form hat Ausbaupotenzial. Aber davon ein andermal mehr.

Die Bindung von Output- an Input-Pin ist denkbar einfach:

c.echo = new RequestResponseChannel(s.EchoFunc);

oder

c.echo = new RequestResponseChannel(s.EchoAction);

Beim Versand von Nachrichten fangen Channels an, ihren Vorteil gegenüber den bisherigen “blanken Drähten” auszuspielen. Sie können entweder ein Resultat so direkt am Ort der Anfrage verarbeiten, wie es eine nachrichtenorientierte Kommunikation erlaubt:

Console.WriteLine("echo result: {0}",
                  echo.Request("hello, echo")
                     
.Receive());

Das ist zwar immer noch ein zweischrittiger Prozess, doch der ist einem Funktionsaufruf noch recht nahe.

Oder Sie verlagern die Verarbeitung in eine Continuation:

echo.Request("hello, echo")
    .Receive(t => Console.WriteLine("echo result: {0}", t));

Das sind dann nicht nur nachrichtenorientiert, sondern sogar schon asynchron aus. Ist es mit EBCs zunächst jedoch nicht.

Die hier beschriebene Request<,>-Nachricht bot zwar auch schon so ein Programmiermodell, doch das war ein Sonderfall. Es musste speziell auf einem Nachrichtentyp implementiert werden. Mit Channels unterscheiden sich Einweg- und Zweiweg-Kommunikation hingegen nicht mehr so sehr. Sie laufen zwar über verschiedene Channels, doch das Programmiermodell ist gleich: in beiden Fällen erfolgt der Versand von Nachrichten mittels Channel-Methoden. Das halte ich für einen Gewinn an Regelmäßigkeit in der Verständlichkeit.

Mehrwegkommunikation

Regelmäßige Kommunikation via Channel macht es dann im nächsten Schritt möglich, auch Komplizierteres zu modellieren. Ich komme zurück auf den obigen Plausch zwischen zwei EBCs. Lassen Sie mich am besten dessen Implementation zunächst zeigen. Der Initiator sieht so aus:

public void A()
{
    using(var x = chat.Open())
    {
        x.Send("hello");
        var i = x.Receive<int>();
        Console.WriteLine("  {0}", i);
        x.Send(i/3.14);
        Console.WriteLine("  {0}", x.Receive<DateTime>());
        x.Send<bool>(true);
    }
}

und sein Gesprächspartner so:

public IEnumerable<ConversationMessage> Chat(IProducerConversation a)
{
    yield return a.WaitFor<string>();
    Console.WriteLine(a.Received<string>());
    conv.Reply(a.Received<string>().Length);
    yield return a.WaitFor<double>();
    Console.WriteLine(a.Received<double>());
    a.Reply(DateTime.Now);
    yield return a.WaitFor<bool>();
    Console.WriteLine(a.Received<bool>());
}

Für beide Konversationspartner macht der Channel es möglich, dass sie trotz Nachrichtenorientierung ziemlich “natürlich” aussehen, d.h. die Sequenz der “Sprechakte” ist klar erkennbar. Sie stehen einfach untereinander in den jeweiligen EBC-Methoden.

Für den Initiator A wäre das auch auf einem Weg wie bei Request/Response möglich gewesen. Aber nicht für seinen Gegenpart. Der wäre ohne Continuations mit einfachen Events nicht möglich. Und das wäre schlecht zu lesen.

Indem X – ich habe die Seite des Gesprächs “Producer” genannt, A ist der “Consumer” oder “Initiator” – jedoch als Iterator ausgelegt ist, kann auch er sequenziell notiert werden. Das ist ein Kleinwenig umständlicher als beim Initiator, doch ich denke, das ist zu verschmerzen.

Dieses Vorgehen habe ich mir bei der Concurrency Coordination Runtime abgeschaut. Funktioniert, wie Sie sehen, aber nicht nur für asynchrone Komponenten gut ;-) Eine Konversation ist dadurch zwar asymmetrisch, doch das finde ich auch nicht schlimm. Axum kann das zwar besser – Sie müssten dafür nur den Preis einer neuen Programmiersprache bezahlen. Angesichts dessen ziehe ich es vor, etwas Umständlichkeit in Kauf zu nehmen.

Den Input-Pin stellt die Methode Chat() dar. Der Output-Pin sieht so aus:

class Client
{
    public ConversationChannel<Conversation> chat;
    …

Indem Sie dem Channel einen Typ für die Kommunikation anhängen, haben Sie die Möglichkeit, eigenen Sende/Empfangsroutinen und weitere Funktionalität darauf zu definieren. Darüber hinaus sehe ich hier auch die Chance für die Definition eines Protokolls; Ihr Conversation-Typ könnte z.B. bestimmen, dass auf eine string-Nachricht vom Initiator ein double folgen muss. Sendet er etwas anderes, liegt ein Fehler vor. Dafür habe ich mir aber noch keine weitere Notation ausgedacht. Später einmal mehr dazu.

Die Verbindung der Pins ist wie bei den anderen Channels ganz einfach:

c.chat = new ConversationChannel<Conversation>(s.Chat);

Wenn eine Konversation umfangreicher wird, können Sie natürlich auch überlegen, ob Sie sie mit einer IBC lösen; ein Interface mag eine natürlichere Ausdrucksform dafür sein. Allerdings verlieren Sie dann den recht einfachen Migrationspfad zur Asynchronizität. Ich ich vermute, dass es dann auch nicht mehr so einfach ist, den Austausch über ein Protokoll zu regeln.

Fazit

Für mich fühlt sich die Einführung von Channels in die EBC-Kommunikation stimmig an. Sie schließt die bisherigen Events nicht aus und führt die Eventhandler ein Stück aus ihren Beschränkungen. Indem Channels Methoden anbieten, ermöglichen Sie auf der Basis einer erweiterten Regelmäßigkeit deutlich mehr Flexibilität für die Gestaltung der Kommunikation.

Channels erweitern das EBC-Paradigma ganz natürlich.

6 Kommentare:

Andre hat gesagt…

Gibts zu der Klasse RequestResponseChannel auch ein Implementierungsbeispiel?
Und ist so ein Channel auch noch EBC 2.0 konform?

Ralf Westphal - One Man Think Tank hat gesagt…

@Andre: Hm... ich hatte den mal näher in der dotnetpro erklärt, glaub ich. Ansonsten aber keine Sourcen. Ist ja aber einfach zu bauen.

Nur die Frage: Warum sollte man ihn bauen? :-) Ich habe ihn seitdem nicht wieder benutzt. EBC bzw Flow-Design hat sich anders entwickelt. Request/Response ist out.

Andre hat gesagt…

Bei dem ganzen EBC und FD Thema habe ich noch nicht ganz verstanden wie Getter/Setter umgesetzt werden sollten. Bei EBC 1.0 war das ja noch eher umständlich.

Ralf Westphal - One Man Think Tank hat gesagt…

@Andre: Es gibt in EBC keine getter/setter. Die haben nichts mit Flow zu tun.

Heiko S. hat gesagt…

Hallo, habe mir in den letzten Tagen vieles über EBC reingezogen, und nur hier werden Channels einmalig erwähnt. Der Blog ist ja aus dem Jahre 2010 - werden nun Channels weiterhin empfohlen oder nicht?
Wenn ja, wie ist der interne Aufbau des Channels, gibt es evtl. ein Interface?

Auch lese ich ab und zu etwas über EBC 2.0, aber wo ist denn (zentral) definiert was 1.0 und was 2.0 ist?

Als ich mit dem Lesen der Dokumente begann, war alles relativ klar, aber so nach und nach kehrte immer mehr Unsicherheit ein. Bestes Beispiel genau hier: "EBC bzw. FD hat sich anders entwickelt. Request/Response ist out".
Davon hab ich bei der ganzen Leserei nicht wirklich etwas mitbekommen :-(

Gibt es denn eine Quelle wo komprimiert die aktuellen Eigenschaften der EBC "Norm" genannt sind?
Oder kann man definieren, dass alle Blogs(dotnetpro-Artikel vor einem bestimmten Datum als "veraltet" gelten?

Gruss Heiko

Ralf Westphal - One Man Think Tank hat gesagt…

@Heiko: Die Kanäle kommen nicht wieder vor. Sind ne nette Idee, aber nicht wirklich wichtig - bisher.

Einen Überblick über das zu Flow-Design geschriebene findest du hier: http://clean-code-advisors.com/ressourcen/flow-design-ressourcen

Ansonsten hilft, die dotnetpro, mein Blog und die Diskussionsgruppen zu verfolgen.

EBC 2.0 ist ein Begriff für den Wechsel von Akteuren zu Aktionen, d.h. die Benennung von Funktionseinheiten mit Verben statt Substantiven.