zurück zum Artikel

Event Based Components: Architektur von Software in neuem Licht

Golo Roden

Je weniger Abhängigkeiten innerhalb von Code bestehen, desto einfacher fallen dessen Wartung und Pflege aus, weswegen Entwickler auf Komponentenebene oft auf das Entwurfsmuster "Inversion of Control" zurückgreifen. Doch genügt diese Art der Entkopplung, um isolierte Komponenten zu entwickeln?

Je weniger Abhängigkeiten innerhalb von Code bestehen, desto einfacher fallen dessen Wartung und Pflege aus. Dieser hinlänglich bekannten Erkenntnis begegnen Entwickler auf Komponentenebene in der Regel mit dem Entwurfsmuster "Inversion of Control", indem sie beispielsweise einen Dependency-Injection-Container (DI) zur dynamischen Auflösung der Abhängigkeiten verwenden. Doch genügt diese Art der Entkopplung, um isolierte Komponenten zu entwickeln?

Die Anhänger Event-basierter Programmierung – siehe Ted Faisons "Event-Based Programming" (apress 2006) – und des Flow-orientierten Designs – siehe J. Paul Morrisons "Flow-Based Programming [1]" (CreateSpace 2010) – verneinen das. Ihre Argumentation lautet, dass die gängige Vorstellung komponentenorientierter Entwicklung noch nicht genügt, um Komponenten wirklich voneinander zu isolieren. Stattdessen tut eine Kopplung über einen neuen Mechanismus Not – eben über Events, sodass sie ihre Komponenten als Event-Based Components (EBC) bezeichnen.

Das eigentliche Problem der bisherigen Komponentenorientierung gestaltet sich dabei wie folgt: Die landläufige Vorstellung einer Komponente basiert auf zwei Forderungen. Die erste lautet, dass sich eine Komponente gegen eine andere mit gleichem Kontrakt austauschen lässt. Dieser Austausch ist durch den gleichen Kontrakt für den Verwender frei von Implikationen und erscheint daher transparent. Die zweite Forderung bedingt die Implementierung einer Komponente als Blackbox. Für ihre Verwendung genügt also die Kenntnis des Kontrakts, die interne Implementierung hingegen ist nur für den Entwickler der Komponente von Belang, für den Verwender jedoch irrelevant.

Auch wenn diese Definition nicht jeglichen formalen Anspruch erfüllt, genügt sie dem täglichen Bedarf zahlreicher Entwickler. Das liegt in der Einfachheit der Definition begründet, schließlich ist der Satz: "Eine Komponente ist austauschbar und wird über einen Kontrakt definiert sowie als Blackbox implementiert", leicht zu merken und enthält zudem alle wesentlichen Aussagen.

Gelegentlich wird allerdings nach einer anschaulichen Analogie aus der wirklichen Welt gefragt. Die vermutlich am häufigsten genannte Antwort auf diese Frage lautet, dass Komponenten wie Lego seien. Die Popularität des Vergleichs liegt wiederum in seiner Einfachheit begründet: Schließlich sind auch Lego-Bausteine austauschbar, sie verfügen über einen gemeinsamen Kontrakt und werden als Blackbox implementiert. Doch ist diese Analogie wirklich passend?

Ihrer ursprünglichen Vermutung und der dieser Analogie allgemein entgegengebrachten Sympathie zum Trotz entscheiden sich viele Entwickler im Lauf der Zeit gegen den Vergleich. Allerdings ist kaum ein Entwickler in der Lage, hierfür rationale Gründe zu benennen oder argumentativ zu begründen, warum ihm die Analogie unpassend erscheint – die Entscheidung dagegen fällen sie allein intuitiv und emotional.

Der offensichtliche Unterschied zwischen Komponenten und Lego-Bausteinen besteht darin, dass die einen dynamischer, die anderen hingegen statischer Natur sind. Die Art der Funktionserfüllung ist also eine andere. Doch auch bei näherer Betrachtung ergeben sich keine Hinweise, warum die Analogie unpassend erscheint – schließlich hängt die Funktionserfüllung weitgehend von der internen Implementierung ab, und von dieser abstrahieren sowohl Komponenten als auch Lego-Bausteine durch den jeweils von ihnen verwendeten Kontrakt. Zudem widerspricht die zu Beginn angesprochene weit verbreitete Verwendung von Dependency-Injection-Containern der Vermutung, dass das Problem an dieser Stelle liegen könnte – dienen sie doch dazu, funktionale Abhängigkeiten aufzulösen.

Problematischer erscheint da bereits die Abhängigkeit einer Komponente von ihrem aktuellen Zustand. Da dieser in der objektorientierten Entwicklung per Definition als "privat" angesehen wird, ist es nur auf Umwegen möglich, auf ihn von außen zuzugreifen. Das ist allerdings in Unit-Tests von Nöten, will man damit einzelne Funktionen einer Komponente unabhängig voneinander testen. Hier helfen nur leidige Workarounds [2]:

Als Ausweg besteht letztlich nur die Möglichkeit, Zustand konsequent als Abhängigkeit zu betrachten und ihn – ebenso wie funktionale Abhängigkeiten – von außen per Dependency Injection injizierbar zu gestalten [5]. Diese Variante ist deutlich aufwendiger, packt das Problem jedoch zumindest an der Wurzel an und stellt die "sauberste" der vorgestellten Varianten dar. Allerdings verfügt sie neben dem hohen Aufwand noch über einen weiteren Nachteil: Sie erfordert die Einführung eines zusätzlichen, nur für Tests gedachten Konstruktors. Die Tests verwenden also nicht mehr den gleichen Konstruktor, der im Produktivcode zur Anwendung kommt.

Es lässt sich festhalten, dass funktionale Abhängigkeiten und die Existenz von privatem Zustand es schwierig gestalten, Komponenten so einfach handhabbar zu machen wie Lego-Bausteine. Doch angenommen, Dependency Injection würde tatsächlich konsequent angewandt, um sämtliche funktionalen und zustandsbehafteten Abhängigkeiten aufzulösen, würden sich Komponenten dann wie Lego-Bausteine anfühlen? Jede Komponente für sich genommen vermutlich ja, doch im Zusammenspiel mangelt es. Denn es kommt eine weitere Art der Abhängigkeit zum Tragen: die sogenannte "topologische Abhängigkeit".

Unter einer topologischen Abhängigkeit versteht man die Abhängigkeit einer Komponente von ihrer Umgebung beziehungsweise von den sie umgebenden Komponenten. Dass diese Abhängigkeiten bei Software existieren, steht außer Frage. Als einfaches Beispiel sei eine Komponente A genannt, die Funktionen einer Komponente B verwendet: Ohne eine Instanz von B ist A nicht lauffähig, was zumindest zur tatsächlichen Laufzeit der Anwendung nicht übermäßig tragisch erscheinen mag.

Allerdings manifestiert sich die topologische Abhängigkeit spätestens beim Testen. Entweder ist dann nämlich neben Komponente A auch Komponente B zu instanziieren (die ihrerseits womöglich wiederum eine Komponente C benötigt), oder es ist ein dediziertes Mock-Objekt zu erzeugen, das als Platzhalter für Komponente B dient. Ersteres macht das für einen Test erforderliche Set-up je nach Länge der Abhängigkeitsketten enorm aufwendig, Letzteres erfordert das zusätzliche Erzeugen zahlreicher Dummy-Objekte, was letztlich wieder aufwendig ist. Auch die vielfach angebotenen Mocking-Frameworks bewirken hierbei eher Linderung als Heilung.

Kurzum, Komponenten fühlen sich nicht wie Lego-Bausteine an, weil sie topologisch von den sie umgebenden Komponenten abhängig und nicht isoliert sind. Lego-Bausteinen hingegen ist es vollkommen gleich, ob sie einzeln oder im Verbund verbaut werden – und wenn im Verbund, spielt die Art des Verbunds für den einzelnen Lego-Baustein keine Rolle. Dependency Injection ist also ein, aber nicht das Hilfsmittel zur Entkopplung. Die Frage lautet also: Wie lassen sich topologische Abhängigkeiten beheben?

Statt zu versuchen, eine neue Lösung von Grund auf zu finden, bietet es sich an, zunächst einmal zu prüfen, wie die prinzipiell gleiche Aufgabe in anderen Domänen gelöst wird. Hierfür drängt sich der Vergleich mit der Elektrotechnik auf, in der einzelne Chips miteinander verdrahtet und als größere Einheiten auf einer Platine untergebracht werden. Chips fühlen sich viel eher wie Lego-Bausteine an, denn auch einem Chip ist es gleich, ob er einzeln oder im Verbund genutzt wird. Was zeichnet einen Chip demnach aus?

Ein Chip ist eine Komponente, die einem Kontrakt genügt und als Blackbox implementiert ist – die klassische Definition einer Software-Komponente also. Der Unterschied zwischen einem Chip und einer klassischen Softwarekomponente liegt in der Verarbeitung der Signale. Ein Chip besitzt Ein- und Ausgänge für elektrische Signale. Diese sind jedoch immer unidirektional: Ein Eingang nimmt stets nur Daten entgegen, ein Ausgang gibt stets nur Daten zurück. Etwas wie einen "Rückgabewert" gibt es nicht – hierfür benötigt man einen gesonderten Ein- und Ausgang.

Genau das ist der Grund, warum Chips isoliert von ihrer Topologie sind: Sie erhalten von irgendwo Signale, und sie senden ihrerseits Signale nach irgendwo. Ob vor beziehungsweise nach ihnen ein anderer Chip sitzt, ob das unterschiedliche Chips sind oder jeweils der gleiche, welcher Art der vorherige beziehungsweise der nachfolgende Chip sind, all das ist ihnen gleich. Die einzige Bedingung zur Zusammenarbeit für Chips lautet, dass die jeweiligen Signale auf die gleiche Art interpretiert werden, damit ein sinnvolles Ergebnis erlangt wird. Was liegt also näher, als zu versuchen, dieses Konzept auch auf Softwarekomponenten zu übertragen?

Die Frage, wie ein solcher Eingang (im Folgenden als Input-Pin bezeichnet) und ein solcher Ausgang (Output-Pin genannt) aussehen könnten, lässt sich zumindest für den Input-Pin leicht beantworten: Bei ihm, per Definition unidirektional, handelt es sich um eine Methode, die zwar Parameter entgegennimmt, aber void als Rückgabetyp verwendet. Auf die Art lassen sich Daten in die Komponente hineinbringen. Die Übertragung ist jedoch unidirektional.

Die Definition eines Output-Pins ist ein wenig schwieriger, da hierfür keine Methode verwendet werden darf, ansonsten wäre die Komponente wieder von der Komponente abhängig, an der die Methode aufgerufen wird. Die Lösung für Output-Pins lautet, sie als Events zu implementieren: Eine Komponente löst dann bei Vorhandensein berechneter Daten lediglich ein Event aus, ohne zu wissen, ob sich eine andere Komponente dafür interessiert – oder wie viele Komponenten oder gar welche Komponenten. Der Trick zur Entkopplung von der Topologie liegt also in der Verwendung von Events, weshalb diese Modellierung von Komponenten als Event-Based Components (EBC) bezeichnet wird. Die Vorteile liegen auf der Hand:

Mit EBCs entsteht ein flexibles Modell von Komponenten, die sich jederzeit neu verdrahten lassen, ohne dass das Einfluss auf die eigentlichen Komponenten hätte – lediglich der Datenfluss wird neu formuliert. Dadurch fällt es leicht, EBC-Anwendungen zu einem späteren Zeitpunkt zu erweitern. Alles, was hierfür notwendig ist, ist das Neuverdrahten der Ereignisse. Die bestehenden Komponenten bleiben hingegen unangetastet.

Abschließend folgt nun noch ein kleines Beispiel, das die Herangehensweise demonstrieren soll. Es entspricht der klassischen "Hallo Welt"-Anwendung, die zunächst aus zwei EBCs und einer Platine modelliert wird: eine EBC zum Erzeugen des auszugebenden Texts, eine zweite EBC für die eigentliche Ausgabe und eine Platine, um beide Komponenten miteinander zu verdrahten.

Die erste EBC nimmt keine Daten entgegen – sie wird nur aufgefordert, den auszugebenden Text zu generieren. Sobald das abgeschlossen ist, löst sie ein entsprechendes Ereignis aus:

public class HalloWeltGenerator
{
public event Action<string> Generated;

public void Generate()
{
string text = "Hallo Welt!";
this.OnGenerated(text);
}

protected virtual void OnGenerated(string text)
{
var handler = this.Generated;
if(handler != null)
{
handler(text);
}
}
}

Diese EBC ist geradlinig und ohne aufwendiges Beiwerk wie Abhängigkeiten von Schnittstellen oder einer speziellen Basisklasse entwickelt: ein den auszugebenden Text erzeugendes Input-Pin namens Generate und ein Output-Pin, an den der erzeugte Text übergeben wird, sobald er erzeugt wurde. Um sie zu testen, muss lediglich ein Unit-Test die Generate-Methode aufrufen und prüfen, ob das Event mit den richtigen Daten ausgelöst wurde. Im Folgenden ein NUnit-Beispiel:

[Test]
public void GenerateCreatesHalloWeltAndRaisesTheGeneratedEvent()
{
var generator = new HalloWeltGenerator();

int count = 0;
generator.Generated += t => {
Assert.That(t, Is.EqualTo("Hallo Welt!");
count++;
}

generator.Generate();
Assert.That(count, Is.EqualTo(1));
}

Auch der Unit-Test lässt sich ebenso geradlinig schreiben, ohne das Wissen darüber haben zu müssen, mit welchen anderen Komponenten die zu testende EBC später einmal zusammenarbeiten soll. Sie kann der Entwickler perfekt isoliert testen. Die Implementierung der zweiten EBC, die die Ausgabe übernehmen soll, erfolgt ebenso leicht:

public class ConsoleWriter
{
public void Write(string text)
{
Console.WriteLine(text);
}
}

Diese EBC verfügt lediglich über einen Input-Pin, ein Output-Pin ist nicht erforderlich. Ein Unit-Test ist daher nicht erforderlich, da die Methode Write nur Framework-Funktionen aufruft – und man kann ohne Weiteres davon ausgehen, dass sie korrekt implementiert sind.

Letztlich fehlt dann nur noch die Verdrahtung der beiden EBCs, was eine Platine übernimmt:

public class HalloWeltBoard
{
private HalloWeltGenerator _halloWeltGenerator;
private ConsoleWriter _consoleWriter;

public HalloWeltBoard()
{
this._halloWeltGenerator = new HalloWeltGenerator();
this._consoleWriter = new ConsoleWriter();

this._halloWeltGenerator.Generated += this._consoleWriter.Write;
}

public void Run()
{
this._halloWeltGenerator.Generate();
}
}

Die Implementierung der Platine erfolgt ebenfalls geradlinig: Mehr als die Instanziierung und Verdrahtung der einzelnen EBCs ist nicht erforderlich.

Es liegt auf der Hand, dass die Erweiterung dieser Anwendung ein Kinderspiel ist, was die folgenden beiden Beispiele demonstrieren sollen:

Besonders hervorzuheben ist, dass im Gegensatz zur klassischen Komponentenorientierung noch keinerlei Interface erzeugt und noch keine Dependency Injection genutzt wurde, und zwar deshalb, weil es bis hierhin nicht notwendig war. Soll man einzelne EBCs gegeneinander austauschen, bietet sich das natürlich wiederum an. Beispielsweise könnten die Klassen HalloWeltGenerator und HelloWorldGenerator das Interface IHelloWorldGenerator implementieren, wobei die Platine nur noch auf IHelloWorldGenerator arbeitet. Die konkret zu verwendende Instanz würde man der Platine als Konstruktorparameter übergeben und per Dependency Injection injizieren:

public class HalloWeltBoard
{
private IHelloWorldGenerator _helloWorldGenerator;
private ConsoleWriter _consoleWriter;

public HalloWeltBoard(IHelloWorldGenerator helloWorldGenerator)
{
this._helloWorldGenerator = helloWorldGenerator;
this._consoleWriter = new ConsoleWriter();

this._helloWorldGenerator.Generated += this._consoleWriter.Write;
}

public void Run()
{
this._helloWorldGenerator.Generate();
}
}

So erhält man mit EBCs ausgesprochen flexible Anwendungen, deren einzelne Komponenten isoliert voneinander testbar sind und die sich ohne Anstrengungen erweitern lassen.

Nicht verschwiegen werden soll der derzeit größte Nachteil von EBCs: Es mangelt an Unterstützung durch vernünftige grafische Editoren. Die im "Hallo Welt"-Beispiel verwendete Verdrahtung lässt sich einfach nachvollziehen, da nur zwei Komponenten involviert sind. Solange die Verdrahtung sequenziell erfolgt, lässt sie sich auch mit mehr als zwei Komponenten problemlos nachvollziehen.

Schwierig wird es jedoch, wenn die Verdrahtung in Schleifen oder parallel erfolgt. Das lässt sich in sequenziell abgearbeitetem Code nicht mehr ohne Weiteres nachvollziehen. Ein grafischer Editor, der sich vom Handling an dem der Workflow Foundation orientiert, wäre hiefür enorm hilfreich. Zwar gibt es in der Community einige Ansätze (etwa Event-Based Components Tooling [6] und Event-Based Components Binder [7]) hierfür, wirklich ausgereift und komfortabel ist derzeit aber noch keines der angebotenen Tools.

EBCs bieten jedoch weitaus mehr Vor- als Nachteile: einfacher Entwurf, einfache Testbarkeit, perfekte Analogie von Modell und Code sowie eine einfache Erweiterbarkeit. Der Schlüssel zum Verständnis von EBCs liegt letztlich in der Aussage: Im Gegensatz zu klassischen Komponenten rufen EBCs keine Methoden anderer Komponenten auf, sondern stellen nur den Wunsch nach Weiterverarbeitung ihrer Daten in den Raum. Wer das einmal verinnerlicht und das Konzept der EBCs verstanden hat, wird Architektur von Software in einem ganz neuen Licht sehen.

Golo Roden
ist freiberuflicher Wissensvermittler und Berater für .NET, Codequalität und agile Methoden. Er betreibt die Website guidetocsharp.de [8] und ist in der myCSharp.de [9]-Community aktiv.

(ane [17])


URL dieses Artikels:
https://www.heise.de/-1240993

Links in diesem Artikel:
[1] http://jpaulmorrison.com/fbp/
[2] http://www.des-eisbaeren-blog.de/post/2010/08/07/Privaten-Code-testen.aspx
[3] http://en.wikipedia.org/wiki/Magic_string_%28programming%29
[4] http://www.aspnetzone.de/blogs/peterbucher/archive/2009/12/04/internalsvisibletoattribute-und-signierte-assemblies-so-funktionierts-garantiert.aspx
[5] http://ralfw.blogspot.com/2009/11/zustand-als-abhangigkeit-ioc-konsequent.html
[6] http://ebclang.codeplex.com/
[7] http://ebcbinder.codeplex.com/
[8] http://www.guidetocsharp.de/
[9] http://www.mycsharp.de/wbb2/
[10] http://jpaulmorrison.com/fbp/
[11] http://www.des-eisbaeren-blog.de/post/2010/07/28/Erster-Eindruck-von-Event-Based-Components.aspx
[12] http://www.des-eisbaeren-blog.de/post/2010/07/29/Welchen-Nutzen-bieten-EBCs.aspx
[13] http://www.des-eisbaeren-blog.de/post/2010/08/08/Terminologie-von-EBCs.aspx
[14] http://ralfw.blogspot.com/2010/02/event-based-components-der-nachste.html
[15] http://ralfw.blogspot.com/2010/02/steckspiele-event-based-components.html
[16] http://www.aspnetzone.de/blogs/juergengutsch/archive/2010/08/18/event-based-components.aspx
[17] mailto:ane@heise.de