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…
13 Kommentare:
Hallo Ralf,
vor ein paar Tagen hat mich Peter Bucher auf ebendieses Buch aufmerksam gemacht, wir fanden es beide interessant und es liegt nun bereit, gelesen zu werden.
Eventuell wären mir Deine Ausführungen nach der Lektüre klarer, doch im Moment muss ich noch ohne vorlieb nehmen.
Prinzipiell schlägst Du ja drei Sachen vor:
1. Parameter eingehender Kommunikation werden in einem Kommando-Objekt gekapselt.
2. Der Rückgabetyp ist immer void.
3. Ergebnisse werden - wenn es sich um eine Query und kein Command handelt - per Event "verschickt", wobei ebenfalls wieder ein Kommando-Objekt verwendet wird.
Prinzipiell kann ich das alles nachvollziehen, und finde es zunächst - wie Du ja selbst schreibst - nicht sonderlich spektakulär.
Allesdings habe ich noch zwei Fragen dazu:
1. Welchen Vorteil bringt es, alle eingehenden Parameter in ein gemeinsames Objekt zu kapseln? Dieses Verfahren wendet man ja auch ohne EBC manchmal an, wenn die Anzahl der Parameter zu hoch ist oder viele Änderungen zu erwarten sind, damit man nicht ständig die Signatur der Methode anpassen / erweitern muss. Doch warum aus diesem => manchmal <= sinnvollen Ansatz eine Regel machen? Wo liegt der Vorteil, wenn ich wirklich ALLES in ein Optionsobjekt verpacke (und um nichts anderes handelt es sich ja letztlich)?
2. Du hast bemängelt, dass Komponenten bislang Abhängigkeiten haben dürfen, was aber eigentlich der Idee des "Lego-Bausteins" widerspricht. Bislang sehe ich den Zusammenhang zwischen der Idee, Komponenten stärker zu isolieren und den EBCs noch nicht. Inwiefern kann ich die Abhängigkeiten durch die Verwendung des von Dir vorgestellten Musters verringern? Salopp gesagt: Wieso brauchen die Kommando-verarbeitenden Methoden weniger Abhängigkeiten, nur weil die Parameter anders gewählt werden?
Was ich letzten Endes also nicht schaffe, ist, Dein Beispiel bestehend aus Compiler, Parser und Codegenerator derart auf EBC umzusetzen, dass auch nur einer der fünf Kritikpunkte entfallen würde.
Kannst Du mir hier eventuell auf die Sprünge helfen?
Viele Grüße,
Golo
PS: Nichtsdestotrotz ein sehr spannendes Konzept, das mich an das Muster des Pipelinings und das Erstellen von Workflows an Hand von Prozessor-Komponenten erinnert ...
Hallo Ralf,
Ich finde den Ansatz sehr spannend, da er mal über den Tellerrand von DI etc. hinausschaut und beleuchtet, wie man es noch anders machen kann. Sehr schön! Ich will den Ansatz auf jeden Fall mal in einem Demoprojekt umsetzen und vll. einige Blogbeiträge (www.minddriven.de) dazu schreiben.
Kennst du das Reactive Framework (Rx)? Dieses würde bei diesem Ansatz passen wie die Faust auf's Auge! Statt dem Event würdest du dann ein IObservable bereitstellen, auf welchem du ein Subscribe() ausführen könntest.
@Golo: ach, du auch hier? ;-)
Zu 1.) Die Grundidee ist imho der Nachrichten-basierte Ansatz. D.h. wir reden nicht über Methoden und deren Parameter, sondern über die Nachricht IncomingCommand (oder besser IncomingMessage?) und eine Schnittstellenmethode, welche diese konsumiert.
Zu 2.) Hier würde ich Golo zunächst recht geben. Ein eventbasierter Ansatz löst zunächst nicht von den Abhängigkeiten los. Da fehlt noch ein wichtiges Puzzlestück: die Weiterleitung und Verteilung von Nachrichten. Um hier die Abhängigkeiten los zu werden, müsste z.B. ein Bussystem eingesetzt werden. Doch dann frage ich mich: warum überhaupt explizit Events publizieren/konsumieren?
Golo's Kommentar (danke!) hat mir gezeigt, dass da noch einige Fragen offen sind. Ich bin mal auf die Antwort gespannt.
Grüße, Matthias
Hallo Matthias,
ja - natürlich ich auch hier ;-).
Ad 1: Wo ist aber letztlich der Unterschied zu dem Aufruf einer Methode, der ich ein Parameterobjekt übergebe, das alle für die Methodenverarbeitung notwendigen Parameter enthält? Nichts anderes ist ein Kommando doch letztlich - oder übersehe ich hier etwas wesentliches?
Viele Grüße,
Golo
Hallo Golo,
Wenn du die Kopplung z.B. über nServiceBus herstellst (um mal eine konkrete Technologie zu nennen), dann geht die Kommunikation immer nur über ganzheitliche Messages, d.h. Abstraktionen von deinen Parametern. Ansonsten würde ich dir rechtgeben: wenn man nicht in Nachrichten denkt, dann ist man schnell wieder bei normalen Parametern...
Grüße, Matthias
Hi Matthias,
korrigiere mich, wenn ich falsch liege (mit Servicebus-Systemen im Allgemeinen und nServiceBus im Speziellen habe ich so gut wie keine Erfahrung), aber geht es bei Servicebus-Systemen mit nachrichtenorientierter Kommunikation nicht eh um asynchrone Kommunikation?
Und Ralf bezog sich ja explizit auf synchrone Kommunikation, zudem sind Komponenten für mich erst mal nur innerhalb einer Anwendung zu finden - wenn es über Anwendungsgrenzen hinweg geht, kommunzieren Komponenten ja nicht mehr direkt miteinander, sondern Anwendungen mit Anwendungen.
Das mag jetzt spitzfindig erscheinen, macht IMHO aber einen wesentlichen Unterschied, und klar, man kann einen Servicebus auch anwendungsintern nutzen, aber das ist ja zumindest derzeit doch eher noch die Ausnahme.
Ich bin ja mal gespannt, wie Ralf unsere Punkte bzw Fragen sieht ...
Viele Grüße,
Golo
@Golo: Einige deiner Fragen mögen sich durch mein aktuelles Blogposting schon geklärt haben.
Hier aber trotzdem nochmal kurz: Warum Methoden auf 1 Parameter beschränken?
Weil damit alles möglich ist.
Ich komme immer mehr zur Geradlinigkeit und klaren Regeln. Und die Regel "Input und Output sind jeweils 1 Parameter" ist klar und geradlinig und schränkt niemanden ein.
Außerdem: Wer so lernt schon bei sync Kommunikation zu denken, der hat später viel weniger Probleme, async und gar verteilt zu denken. Und genau darum geht es mir. Ein sanft ansteigender Pfad von lokaler sync Kommunikation hin zu verteilter async Kommunikation.
Es geht also mal wieder nicht darum, was möglich ist, sondern was systematisch Verständlichkeit und Evolvierbarkeit und Korrektheit fördert.
-Ralf
Hallo Ralf,
ich habe gerade Deinen neuen Eintrag gelesen, und Du hast nicht nur meine beiden offenen Fragen beantwortet, sondern mich auch zum Staunen gebracht.
Zum Staunen deshalb, weil ich beeindruckt bin von dem Konzept an sich, seiner Leichtigkeit und zugleich dem Grad der Unabhängigkeit, der sich erreichen lässt ...
So skeptisch ich nach diesem Beitrag war, so gut gefällt mir das ganze jetzt bereits :-).
Viele Grüße,
Golo
Ist dieser Ansatz nicht sehr ähnlich dem 'Signal & Slot'-Konzept der Qt Bibliothek?
Wenn ich mir das so überlege, wundert es mich, dass sich diese Art der "Vernetzung" von Komponenten bislang nicht durchsetzten konnte.
Gibt es da etwa ein Akzeptanzproblem?
@Andreas: Danke für die Hinweis auf Qt. Ja, sieht so aus, als wollten EBCs dasselbe wie Qt mit seinen Signals und Slots (http://de.wikipedia.org/wiki/Signal-Slot-Konzept). Sehr cool.
Warum hat das bisher keine größere Verbreitung gefunden? Die C++ Gemeinde ist halt sehr für sich; da fließt dann nicht unbedingt etwas zum Rest der Programmierwelt. Dann sind Signals/Slots auch dort wieder - so scheint es - eine Sache, die mit GUI assoziiert ist. Aber warum?
Mir scheint, bisher haben solche sync ereignisorientierten Konzepte keine größere Verbreitung gefunden, weil sie im Schatten von GUIs stehen und natürlich eine Umgewöhnung bedeuten. Es geht ja anders, direkter. Da fragt sich Joe Developer schon, warum er so einen Aufstand machen soll.
Aber das entmutigt mich nicht. Der Druck ist heute höher. Evolvierbarkeit wird immer wichtiger. Die Zeit für Ereignisorientierung auf breiterer Basis ist gekommen. Auch wenn es ein wenig mehr Mühe machen sollte.
Schön, dass es erfolgreiche Beispiele in anderen Bereichen gibt. Jetzt muss nur eine Übersetzung in die .NET Welt stattfinden.
-Ralf
Hi Ralf,
ich hatte dir ja erzählt, dass ich EBC auf PHP umgesetzt hab, damit ich auch den Service meiner Lesezeichenanwendung in EBC umbauen kann. Und ich muss sagen, dass ich gelinde gesagt begeistert bin. Jede Komponente wie Deserializer oder Authenticator ist jetzt getestet. (Vorher gar nicht).
Warum jetzt schon? Weil das einfach total cool geht, weil jede Einheit für sich ist und für sich - ohne Abhängigkeiten - getestet werden kann.
Länger überlegt und ausprobiert habe ich bei der Einheit Configuration, also die Komponente, die Settings etwa für Zugänge oder Pfade liefert. Auch das hab ich anfänglich mit EBC umgesetzt. Eine Komponente, die einen Wert aus Configuration braucht, hat quasi zwei zusätzliche Pins: über den einen fragt sie einen Wert von Configuration ab und über den anderen meldet Configuration diesen zurück.
Das funktioniert, allerdings wird das ganze - zumindest in PHP - ziemlich komplex und ist nicht mehr eingängig.
Also hab ich das wieder umgebaut und injiziere jetzt etwa in den Authenticator die Configuration.
Klar schaffe ich damit Abhänigkeit, aber die ist deshalb hinnehmbar, weil ich Configuration testen kann und eine Komponente, die Config-Settings braucht, nur diese eine Abhängigkeit hat.
Wie siehst du das mit zentralen Einheiten, auf die mehrere Komponenten zugreifen?
Tilman
@tboerner: Ich freue mich sehr, dass die EBCs so gut gefallen.
Aber EBCs sind nicht allein seligmachend. Es kann ruhig weiterhin "traditionelle" Komponenten geben. Bei einem ConfigAdpter könnte das der Fall sein. Den sehe ich als Blatt auch in einer "traditionelle" komponentenorientierten Architektur. Wenn du den also nicht als EBC realisierst, dann hast du kaum Nachteile - aber ein etwas einfacheres Programmiermodell.
Aber Achtung: Bei EBCs ist es auch nicht immer nötig, zwischen zwei Komponenten für einen Request zwei Drähte zu spannen. Mit einem Request<,> Objekt kannst du auch auf einem Draht einen Wert anfragen und bekommen.
Und mit etwas Extension Method Magie kannst du dann fast so programmieren wie "normal":
var authInfo = this.Out_GetConfig.Request(new GetAuthenticationInfo()).Return();
Das ist immer noch EBC - aber sieht ein wenig einfacher aus.
-Ralf
Hi Ralf,
pretty Extension-Method please with sugar on top gibt es leider in PHP nicht. :-)
Aber ja, so könnte es gehen. Wobei man dann tierisch aufpassen muss, dass man nicht in einer Endlosschleife landet.
Tilman
Kein Grund zur Euphorie. Was mit EBCs beschrieben wird, wurde schon 1996 mit den Architekturstilen "Event-Based, Implicit Invocation" und "Pipes and Filters" in dem Buch "Software Architecture: Perspectives on an Emerging Discipline" (ISBN-13: 978-0131829572) angegeben.
In "Pattern-orientierte Software-Architektur" (ISBN-13: 978-3827312822) aus 1998 wird auch das Compiler-Beispiel mit Pipes and Filters behandelt. Interessant ist dort auch die Beschreibung des Event-Channels (OMG) im Rahmen des Publisher-Subscriber-Musters.
Kommentar veröffentlichen
Hinweis: Nur ein Mitglied dieses Blogs kann Kommentare posten.