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 ;-)
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:
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.
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.
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.
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…