Event Based Components: Architektur von Software in neuem Licht
Seite 2: EBC
Input- und Output-Pins fĂĽr Komponenten
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:
- EBCs lassen sich isoliert entwickeln und ohne zusätzlichen Aufwand isoliert testen.
- EBCs können beliebig miteinander verdrahtet werden, solange sich die zu verdrahtenden Komponenten über das Format der Daten einig sind.
- EBCs lassen sich in größeren Einheiten (sogenannten Platinen) zusammenfassen, die ihrerseits wiederum über Methoden und Ereignisse als EBC auf einer höheren Hierarchieebene dienen können.
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:
- Soll der erzeugte String beispielsweise vollständig in Großbuchstaben ausgegeben werden, lässt sich einfach eine weitere EBC implementieren, die über einen Input- und einen Output-Pin verfügt sowie die entsprechende Transformation vornimmt. Die bestehenden EBCs sind hierfür nicht zu verändern. Lediglich die Verdrahtung auf der Platine ändert sich, indem man den bisherigen Fluss umleitet, sodass die neue EBC zwischen die beiden bestehenden geklinkt wird.
- Soll die Anwendung mehrsprachig sein, erzeugt man einfach eine weitere – beispielsweise englischsprachige – Version der Klasse HalloWeltGenerator, die den Text auf Englisch darstellt. Um die Anwendung nun von Deutsch auf Englisch umzuschalten, genügt es wiederum, die Verdrahtung zu verändern. Die eigentlichen Komponenten bleiben unangetastet.
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.