Follow my new blog

Donnerstag, 1. April 2010

Holarchie – Event-Based Components für natürliche Softwaremodellierung

Software ist eine Holarchie, d.h. eine Hierarchie von Teilen, die gleichzeitig Ganzes sind. Das hatte ich während der Modellierung von Software mit “traditionellen” Komponenten schon fast wieder vergessen. Aber neulich schrieb mich jemand zu meiner Blogserie über das Softwareuniversum an, weil er Parallelen zu seiner Arbeit sah. Da habe dann versucht, meinen derzeitigen Ansatz der Event-Based Components (EBC) die damaligen Gedanken anzuschließen.

So sah 2005 eines meiner Diagramme für die grundsätzliche Struktur von Software aus:

image

Die Frage heute: Bin ich diesem “Ideal” nun mit EBCs ein Stück näher gekommen?  Ich will die Antwort anhand eines Beispiels versuchen, für das ich zwei Architekturen entwickle. Eine mit “traditionellen” Komponenten, eine mit EBCs.

Das Szenario ist simpel: Der Kunde wünscht sich einen Funktionsplotter. Er möchte also ein kleines Programm haben, in das er eine Formel eingeben kann – z.B. x^2+2x-3 oder Sqrt(Sin(x)*Cos(x)) –, für die dann ein Graph am Bildschirm geplottet wird.

Traditionelle Komponentenarchitektur

Wie hätte ich eine Architektur für den Funktionsplotter vor einigen Monaten geplant? Ich kann mich kaum erinnern ;-) EBCs haben sich schon so in meinem Denken breit gemacht, dass ich kaum noch ohne kann. Aber ich versuche es einmal.

Am Anfang stünde eine Softwarezellen bzw. ein System-Umwelt-Diagramm:

image

Eine Anwenderrolle greift auf die ganze Anwendung zu. Der Anwender ist abhängig von der Anwendung. Das System-Umwelt-Diagramm ist also ein Abhängigkeitsdiagramm.

Im nächsten Schritt würde ich die Softwarezelle zerlegen in Funktionseineiten. Nach dem Separation of Concerns Prinzip fielen mir da mindestens zwei ein: eine Funktionseinheit für die Interaktion mit der Benutzerrolle, ein Portal, und eine für den Rest, d.h. die Domänenlogik.

image

Bei genauerem Nachdenken würde ich dann natürlich das Portal weiter zerlegen. Eine eigene Funktionseinheit für das Plotten schiene mir angezeigt. Und auch die Logik zerfiele in mindestens zwei Funktionseinheiten:

image

Sollte ich es dabei bewenden lassen? Im Frontend ja. Aber die Logik… Nein, die ist noch zu grob. Ein Formelübersetzer ist – so nehme ich mal für dieses Beispiel an – keine so einfache Sache. Die will sauber in drei Pässe gegliedert sein. Es geht ja um nicht weniger als die Übersetzung von Quellcode (Formel) in Code (ausführbare Funktion). Da soll auch die kleine Plotteranwendung in puncto Systematik nicht hinter einem richtigen Compiler zurückstehen.

Und auch die vorgeschaltete Funktionseinheit scheint nicht sauber. Sie hat noch ein “und” in der Bezeichnung, sie tut also noch zweierlei. Zum einen veranlasst sie etwas (Übersetzung), zum anderen tut sie etwas (Funktionswerte berechnen). Das ist im Sinne des Single Responsibility Principle nicht schön.

Wenn ich diese Gedanken in die Architektur einfließen ließe, würde sie sich so weiter entwickeln:

image

Zwei Funktionseinheiten stechen darin heraus, das Rechenwerk und der Compiler. Beide haben eigentlich keine richtige Aufgabe im Sinne der Geschäftslogik. Sie sind nur Fassaden, die Details rechts von ihnen gegenüber ihren Clients verbergen. Allenfalls koordinieren sie noch die Arbeit der Funktionseinheiten, von denen sie abhängig sind. Das Rechenwerk lässt erst den Compiler die Formel übersetzen und übergibt sie dann an die Funktionswertberechnung. Der Compiler fordert zuerst den Scanner auf, die Formel in Symbole zu zerlegen. Die reicht er weiter an den Parser. Der erzeugt einen abstrakten Syntaxbaum, den der Codegenerator übersetzt.

Im Rahmen “traditioneller” Komponentenorientierung ist das völlig normal. Auf Rechenwerk und Compiler zu verzichten, würde nämlich zu ganz unschönen Abhängigkeiten führen, z.B.

image

Rechenwerk und Funktionswertberechnung hingen nun zusammen und würden nur den Parser ansteuern. Der lieferte ultimativ auch die übersetzte Funktion – aber natürlich nur unter Zuhilfenahme des Parsers, der wiederum den Codegenerator brauchte.

Nein, das sieht ungut aus. Warum soll ein Scanner den Parser kennen und am Ende sogar etwas mit dem generierten Code zu tun haben? Und warum sollte eine Funktionsberechnung einen Scanner kennen? Der ist ein Detail einer bestimmten Compilerimplementation.

Die Funktionseinheiten Rechenwerk und Compiler wären nötig in einer “traditionellen” Komponentenorientierung.

Und wo sind die Komponenten nun? Ich würde sagen, alle Funktionseinheiten verdienen es, als Komponenten realisiert zu werden. Z.B. Compiler, Scanner, Parser und Codegenerator in einer Komponente zusammen zu fassen, würde die Möglichkeit zur Parallelentwicklung begrenzen. Denn neben klaren Grenzen zwischen Funktionseinheiten geht es darum bei der Komponentenorientierung: hohe Produktivität, durch gleichzeitige Arbeit an Funktionseinheiten. Rechenwerk und Compiler sind dann zwar recht simple Komponenten, weil sie nur andere integrieren. Doch das macht nichts. Wenn sich z.B. der Ansatz zur Compilation ändert, dann kann wieder mehr Verantwortung in den Compiler wandern. Ihn als Verantwortlichkeit ausdrücklich zu definieren, trägt zur Evolvierbarkeit bei.

Fazit “traditionelle” Komponentenarchitektur

Was gibt es hervorzuheben an einer “traditionellen” komponentenorientierten Architektur? Was gibt es womöglich zu kritisieren? Das ist gar nicht so leicht zu sagen, wenn man tief drin steckt in dem Denken. Denn dann ist vieles ganz natürlich – was eigentlich Ballast ist. Und was fehlt, erkennt man nicht so genau.

In meinem ersten Posting zu EBCs habe ich fünf Aspekte “traditioneller” Komponenten genannt, mit denen ich mich schwer tue. Für das obige Beispiel möchte ich drei davon herausgreifen:

  • Komponenten haben Abhängigkeiten. Abhängigkeiten machen das Entwicklerleben schwer. Wo Abhängigkeiten ins Spiel kommen, wird allemal das Testen schwieriger.
  • Spezifikationen sind nicht kompakt. Die Spezifikation des Compilers besteht z.B. aus seinem eigenen exportierten Kontrakt sowie den importierten Kontrakten der drei Komponenten, von denen er abhängig ist.
  • Die Schachtelung von Komponenten ist schwierig oder gar unmöglich.

Insbesondere die letzten beiden Punkte machen mich bei der obigen Architektur unglücklich. Ich finde es sehr unhandlich, einen Compiler nur implementieren zu können, wenn ich insgesamt vier Kontrakte zur Hand habe. Und warum sollte eine Client-Komponente den ganzen Kontrakt einer Service-Komponente kennen, wenn sie selbst davon nur einen Ausschnitt selbst braucht? Wer ist schon so konsequent und betreibt Interface Segregation, um jedem “Kunden” einer Service-Komponente nur den für ihn passenden Ausschnitt zur Verfügung zu stellen?

Viel schlimmer aber noch ist, dass die finale Architektur nur noch eine Abstraktionsebene im Code enthält. Die Komponenten liegen alle auf demselben Niveau. Ein Betrachter des Codes hat damit keine Möglichkeit, unterschiedliche Betrachtungsabstände einzunehmen, um sich Überblick zu verschaffen oder Details zu betrachten.

Zwar habe ich mich in mehreren Schritten an diese “Blattebene” herangeschlichen. Doch die Funktionseinheiten auf darüber liegenden Abstraktionsniveaus sind nicht mehr sichtbar. Sie existieren nur noch auf einem Blatt Papier – und das ist bekanntlich geduldig.

Aber vielleicht sehen Sie es auch anders und meinen, die Architektur würde immer noch mehrere Abstraktionsebenen enthalten. Scanner, Parser und Codegenerator gehören dann zur untersten Ebene, Compiler liegt darüber und mit der Funktionswertberechnung gleich auf. Darüber dann das Rechenwerk, darüber das GUI. Ja, so könnte man das sehen:

image

Aber ist das wirklich plausibel und hilfreich? Liegt ein GUI auf einem höheren Abstraktionsniveau als ein Compiler? Ist das Abstraktionsniveau von einem Aufgabenbereich oder gar Belang abhängig? Nein, nein, damit kann ich mich gar nicht anfreunden. Außerdem würde das bedeuten, dass Kommunikation nie zwischen Funktionseinheiten auf demselben Abstraktionsniveau abliefe, sondern immer über Niveaus hinweg: der Compiler auf Level n spräche mit dem Scanner auf Level n+1 usw.

image

Das fühlt sich gar nicht gut an.

Abhängigkeiten definieren einen Baum. Entlang der Äste verläuft die Kommunikation. Schwer verständliche Schachtelung ist damit das Fundament der “traditionellen” Komponentenorientierung. Grauslich! Da hilft dann auch keine Dependency Injection.

Das führt übrigens auch dazu, dass so sinnlose Komponenten wie der Compiler oder das Rechenwerk auftauchen. Die tun nichts anderes, als die Kommunikation zwischen anderen Komponenten zu ermöglichen. Vom Standpunkt der Domänenlogik aus gesehen ist das nicht nötig. Aber die vertikalen Abhängigkeiten erzwingen das. Scanner, Parser und Compiler liegen auf demselben Abstraktionsniveau – und kennen sich daher nicht. Widersinnig, oder? Und wenn sie sich kennen wie in der verworfenen Architektur, dann ist das auch nicht glücklich. Denn warum sollte ein Scanner von einem Parser abhängen? Oder von mir aus auch umgekehrt: Warum sollte ein Codegenerator von einem Parser abhängen und der von einem Scanner?

Ein Codegenerator leistet etwas auf einem Input: er verwandelt einen Abstrakten Syntaxbaum (AST) in lauffähigen Code. Diese Leistung hat nichts, aber auch gar nichts mit der Erzeugung eines solchen AST zu tun. Warum sollte also ein Codegenerator die anstoßen?

Also muss ein übergeordneter Compiler her. Der tut nichts anderes, als die Kommunikation zwischen Scanner, Parser und Codegenerator angemessen herzustellen. Aber er ist eine Komponente mit vielen Abhängigkeiten. Er ist vergleichsweise schwierig zu testen und dabei so gar nicht durch die Problemdomäne motiviert. Eine unbefriedigende Situation. Koordinationskomponenten verrauschen das Architekturbild.

Ein Abhängigkeitsdiagramm ist einfach nicht genug, um eine Architektur zu beschreiben. Es ist nötig, um die vielen Kontrakte klar zu kriegen und die Abhängigkeiten sauber zu definieren. Aber wie die Kommunikation läuft, ist daraus nicht ersichtlich. Dafür brauchen wir mindestens einen zweiten Diagrammtyp. Z.B. ein Aktivitätsdiagramm oder ein Datenflussdiagramm müssen die Lücke füllen.

Meine bottom line: Eine unbefriedigende Situation. “Traditionelle” Komponenten sind besser als gar keine Komponenten. Sie befördern die Produktivität, die Flexibilität und die Korrektheit. Doch es bleibt ein Nachgeschmack.

Event-Based Component Architektur

Wie kann es mit der Architektur besser werden als mit “traditionellen” Komponenten? Ich versuche dasselbe Szenario mal mit Event-Based Components (EBC) zu beschreiben. Am Anfang steht wieder ein Systen-Umwelt Diagramm:

image

Das sieht der “traditionellen” Komponentenorientierung noch sehr ähnlich. Allerdings: Beachten Sie, dass auch hier schon von Nachrichten gesprochen wird und keine Abhängigkeit vorhanden sind. Die Anwendung als ganzes wird als Komponente gesehen, die asynchron gegenüber ihrer Umgebung läuft.

Das System-Umwelt Diagramm ist die höchste Abstraktionsstufe. Es besteht ja nur aus einer Funktionseinheit. Diese Funktionseinheit ist natürlich zu groß, um sie einfach so zu implementieren. Also muss ich sie zerlegen.

image

Wenn ich die Applikation “aufmache”, dann finde ich zwei Funktionseinheiten: ein Frontend für die Interaktion mit dem Benutzer und ein Rechenwerk, dass die Formel im angegebenen Wertebereich berechnet. Das ist immer noch eine Ebene, die ein Laie versteht. Eine Benutzeroberfläche ist etwas anderes als die Funktionalität, die Formeln berechnet.

Im Hintergrund habe ich noch die darüber liegende Hierarchieebene “durchscheinen lassen”. Sie soll uns erinnern, dass die beiden Funktionseinheiten Verfeinerungen sind.

Jetzt zur nächsten Ebene hinab steigen:

image

Jede Funktionseinheit der vorherigen Ebene ist verfeinert worden, d.h. zerlegt in mehrere Funktionseinheiten mit spezifischeren Verantwortungen. Diese Subfunktionseinheiten sind den übergeordneten eingeschrieben. Funktionseinheiten können also Container sein. Nicht physische Container, sondern logische, sozusagen “Verantwortungscontainer”.

Das ist ein Aspekt, der mir wichtig geworden ist. Mit EBCs ist echte Schachtelung möglich. Aber das ist keine physische Schachtelung. Wie Implementierungen von Funktionseinheiten physisch geschachtelt sind, halte ich inzwischen für einen ganz anderen Concern. Darüber muss man nachdenken – aber es hat nichts mit der Funktionalität einer Anwendung zu tun. Wie Funktionseinheiten physisch versammelt und geschachtelt werden, ist eine Sache des Entwicklungsprozesses. Darauf wirken vor allem Fragen der Parallelität von Entwicklung, von Risiko, Unsicherheit oder Komplexität/Entwicklungsdauer ein.

Aber nun weiter mit der Modellierung. Das Frontend ist abgeschlossen, doch beim Rechenwerk muss der Compiler noch zerlegt werden:

image 

In dieser Abbildung sind nun alle Abstraktionsebenen ganz natürlich ineinander geschachtelt zu sehen. Je dunkler eine Komponente, desto niedriger ihr Abstraktionsniveau. Echte Domänenfunktionalität enthalten nur die Blätter, d.h. die Funktionseinheiten mit dem niedrigsten Abstraktionsniveau.

Der Schachtelungsbaum dazu sieht so aus:

image

Abhängigkeiten verlaufen in der Vertikalen. Sie sind im Architekturdiagramm implizit. Wenn Komponenten andere enthalten, dann sind sie von ihnen abhängig. Wichtiger ist jedoch, dass die Kommunikation hier horizontal verläuft. Das Architekturdiagramm legt den Schwerpunkt nicht auf Abhängigkeiten, sondern darauf, wie die Funktionseinheiten miteinander kommunizieren.

Wo bei “traditionellen” Komponenten zwei Diagramme nötig sind, reicht bei EBCs ein Diagramm.

Fazit Event-Based Component Architektur

Vergleichen Sie selbst die Diagramme für die “traditionelle” Komponentenarchitektur und die EBC-Architektur. Welches finden Sie aussagekräftiger, verständlicher?

Die Vorteile der EBC-Architektur scheinen mir auf der Hand zu liegen:

  • Der Fokus liegt hier ganz eindeutig auf den Blättern. In denen “spielt die Musik”, dort steckt die Domänenlogik, sie mögen komplex sein. Aber sie haben keine Abhängigkeiten. Deshalb sind sie gut zu testen. Und deshalb lassen sie sich auch vergleichsweise gut wiederverwenden.
  • Abhängig sind “Zwischenebenen” oder “Platinen”. Sie sind von den eingeschachtelten Funktionseinheiten abhängig. Aber das macht nichts. Diese Abhängigkeiten sind sehr einfach. “Platinen” lassen sich aus der Beschreibung der Verbindungen zwischen ihren “Bauteilen” generieren. Das macht ihre Tests sehr einfach.
  • Die Abhängigkeiten sind implizit. Sie verwirren nicht das Verständnis. Die wichtigeren Kommunikationswege stehen im Vordergrund. Die Kommunikation ist horizontal zwischen Funktionseinheiten innerhalb eines “Containers” auf derselben Abstraktionsebene.
  • Last but not least: Die Ebenen, die eine schrittweise Verfeinerung durchläuft, bleiben in der Architektur physisch, d.h. in Form von Artefakten (“Platinen”) erhalten. Das trägt sehr zum Verständnis der Architektur bei. Zoom-in/out ist nicht nur möglich, sondern bezieht sich auf realen Code und nicht nur “Ideen”.

EBC-Architekturen manifestieren für mich die konzeptionelle Holarchie des ersten Bildes oben. Software ist ein System mit beliebig vielen Ebenen. Auf jeder Ebene arbeiten beliebig viele Funktionseinheiten zusammen. Aus Input produzieren sie Output. Sie kennen einander nicht und sind ganz regelmäßig aufgebaut. Das erleichtert ihre Produktion (“Platinen” können generiert werden, Blätter werden parallel handcodiert). Das erleichtert die Komposition von Architektur mit ihnen. Und das ermöglicht den Einsatz von Standardkomponenten.

Nach meinem Empfinden sind wir mit EBCs der lang erprobten Arbeitsweise von Elektrotechnikern und Maschinenbauern so nah wie nie zuvor. Software ist natürlich immer noch anders als physische Bauteile; aber warum soll sie nicht von denen lernen?

Ich jedenfalls kann quasi schon gar nicht mehr anders denken als in EBCs. Und mit jeder Architektur, die ich so entwerfe, wird es einfacher.

Mit EBCs sehe ich Software als Sammlung von Prozessen. Einfachen und komplizierten. Die bestehen aus Verantwortlichkeiten, die zusammen etwas verarbeiten und produzieren. Diese Verantwortlichkeiten und ihre Kooperation beschreiben EBCs.

Objekte und “traditionelle” Komponenten werden dadurch nicht überflüssig. Sie bekommen aber einen anderen Platz. Auch das macht die Architektur einfacher.

Verfeinerung der EBC-Architektur

Zum Schluss noch ein Detail, über das Sie sich keine Gedanken gemacht haben mögen. In der EBC-Architektur ist noch eine kleine Unstimmigkeit, die ich der Einfachheit halber bisher überspielt habe. In das Rechenwerk kommt eine Berechnungsanforderung rein. Die besteht aus einer Formel und einem Wertebereich, auf den die Formel angewandt werden soll, z.B. Formel “2*x” und Wertebereich 0 bis 10 in 100 Schritten.

Durch die Blätter des Rechenwerks fließt bisher nur ein Datenstrom. Das bedeutet, jede Funktionseinheit muss den Wertebereich bis zur Funktionswertberechnung durch schleifen. Was aber soll z.B. ein Scanner mit diesem Wertebereich?

Die Architektur für das Rechenwerk sähe daher besser wie folgt aus:

image

Split und Join sind Standard-EBCs. Sie werden in den Nachrichtenstrom eingesetzt, um Nachrichten zu transformieren und damit passend für folgende Funktionseinheiten zu machen. Das erhöht die composability (Komponierbarkeit) von Architekturen. Funktionseinheiten können leichter wiederverwendet werden, wenn sie ihren Kontrakt nicht an ihre Umgebung anpassen müssen, sondern umgekehrt kleine Standardadapter die Umgebung an eine Komponente anpassen.

Hier noch zwei Ideen, wie solche Standardkomponenten helfen könnten:

image

Zunächst: Bemerken Sie, wie einfach es ist, die Flughöhe zu wechseln? Im vorhergehenden Bild waren wir im Rechenwerk. Jetzt ist das Rechenwerk eine Black Box; wir fliegen höher, sehen ein bigger picture. Das Abstraktionsniveau ist gestiegen.

Auf diesem Level habe ich nun Standardkomponenten eingefügt, die die Performance und Reaktionsfähigkeit des Systems steigern sollen. Ein Cache ist zwischen Frontend und Rechenwerk geschaltet, um zu vermeiden, dass Übersetzungen und Berechnungen, die schon gelaufen sind, nochmal zeitaufwändig ausgeführt werden. Für das Frontend macht dieser Einschub keinen Unterschied. Es ist ja unabhängig von anderen Komponenten. Und auch das Rechenwerk merk davon nichts. Das ist AOP at its best ;-)

Dasselbe gilt für die Asynchronizität. Das Rechenwerk läuft nun auf einem anderen Thread als das Frontend. Berechnungen finden dem gegenüber also im Hintergrund und asynchron statt. Der Benutzer kann im Frontend andere Funktionen aufrufen, während das Rechenwerk seine Arbeit leistet. Das Frontend friert nicht ein.

Falls im Hintergrund ein Fehler auftritt, wird der als eigene Nachricht auf dem Frontend-Thread zurück gemeldet. Und Ergebnisse kommen auch auf dessen Thread an, weil sie ein Synchronization Context Switch aus dem Hintergrund in den Vordergrund holt.

Fühlen Sie die Eleganz des EBC-Ansatzes? Keine der Komponenten kennt/braucht eine andere. Die Verbesserung der nicht funktionalen Eigenschaften des Systems waren deklarativ ohne Veränderung auch nur einer existierenden Komponente möglich.

Und falls sich herausstellt, dass die Kombination Cache+Async öfter nützlich ist, kann sie ganz einfach mit einer “Platine” für die Wiederverwendung zu einer neuen Komponente zusammengeschnürt werden:

image

Wieder würde sich für die anderen Komponenten nichts ändern. Nur die Verdrahtung wäre betroffen, d.h. die “Platine”, die die Komponenten verbindet. Mit einem Architekturtool wäre das aber wohl nur eine Kleinigkeit. Eine simple und typische Refaktorisierung, z.B. “Komponenten zu Platine zusammenfassen”.

Im Sinne des SLA-Prinzips würde ich dann jedoch noch einen Schritt weitergehen:

image

Jetzt ist wieder alles auf demselben Abstraktionsniveau: ein Frontend ist verbunden mit einem Rechenwerk. Wie das genau funktioniert, ist egal. Wen das interessiert, der zoomt in das asynchrone Rechenwerk hinein.

Irgendwie geht das alles natürlich auch mit “traditionellen” Komponenten. Aber elegant finde ich es nicht mehr. Warum sollte ich mir auch nur eine Sekunde lang Gedanken über Abhängigkeiten machen? Warum sollte ich mir eine Sekunde lang Gedanken über die Größe von Interfaces machen?

Ob eine EBC-Komponente “zu groß” ist, sehe ich in einer EBC-Architektur viel leichter als in einer “traditionellen”. Ich muss nur die Input-/Output-Pins zählen. Die geben viel detailliertere und sichtbarere Auskunft über die Verantwortlichkeitsbreite einer Komponente, als ein simpler Pfeil in einem Abhängigkeitsdiagramm.

So, nun aber genug für heute. Lassen Sie sich die Diagramme einmal auf der Zunge zergehen. Ich hoffe, Sie schmecken es, wieviel einfacher, regelmäßiger, verständlicher echt holarchische Software mit EBCs sein kann.

7 Kommentare:

Frank Striegel hat gesagt…

Gratulation, so werden auch komplexe Architekturen beherrschbar. Das ist ein echter Fortschritt! Freue mich bereits auf das erste Projekt, das ich so umsetzen darf.

Gruss
Frank

Laurin Stoll hat gesagt…

Sehr cool. Du redest mir direkt aus dem Herzen! Genau diese Koordinierenden Fassaden in der klassischen Komponentenorientierung, mit denen konnte ich mich nie anfreunden!
Ich finde hinter EBC steckt enormes Potential. Man denke schon nur mal wie simpel das Testing wird.
Hirne viel daran rum, wie man das Tooling noch besser hinkriegen könnte. So wies jetzt ist, ist EBC sehr angenehm zu entwickeln - in den einzelnen Komponenten. Für den Benutzer einer EBC Komponente finde ich es allerdings noch etwas umständlich:

Instanzieren aller EBC's
Verdrahten der EBC's
Event für Resultat behandeln
Anstossen der ersten EBC

Das finde ich noch etwas umständlich.
Genial wiederum ist aber, wie EBC beim Verdrahten aber eigentlich den Programmfluss beschreiben. Überlege mir die ganze Zeit, ob man das nicht irgendwie gut in ein FluentInterface kriegt, damit es einfach ein wenig angenehmer anzusehen ist.

After(reader.OnReadCompleted).Then(validator.Validate)
Naja... ne vielleicht ist das auch nicht besser....
mal sehen was EBC in unseren Köpfen noch so für Wellen schlägt :-)

Ralf Westphal - One Man Think Tank hat gesagt…

@Laurin: Wen meinst du mit Benutzer einer EBC-Komponente? Es gibt keinen. Keine Komponente benutzt irgendeine andere :-) Das ist ja der Trick.

Aber es gibt natürlich einen, der andere kennt, das ist die Platine. Die verdrahtet und das ist, wenn man es von Hand tut, etwas umständlich. Zugegeben.

Die Platine instanziert jedoch nicht, sondern bekommt ihre Komponenten reingereicht (DI).

Es gibt auch kein Problem mit "Event als Resultat" und "Anstoßen".

Resultate sind ja Output und werden verdrahtet.

Die Erzeugung von Komponenten ist Sache des Host, also dem Programm, das gestartet wird. Dort benutzt man einen DI Container um das Mapping aufzubauen. Wie üblich. Und dann wird nur eine "Wurzelplatine" (Mainboard) instanziert. Dadurch wird das ganze System dann aufgebaut. Und auf der Wurzelplatine wird ein Event angestoßen, sowas wie Run(args). Fertig.

Auch wenn die Verdrahtung etwas mühsam ist, halte ich den Gesamtgewinn für so groß, dass ich diese Mühe gern auf mich nehme. Und irgendwann haben wir Tools, die es uns einfacher machen. Keine Sorge.

-Ralf

Laurin Stoll hat gesagt…

@Ralf:
Ich dachte jetzt an eine Form die ein Button hat und dann was macht. Hier mal ein kleines EBC Sample mit welchem ich gerade rumspiele.
Schau mal - ich habe das jetzt in dem Sample mal frech in den Eventhandler gemacht. Klar könnte ich das in einen Presenter respk. von da noch in eine 'Platinenklasse' auslagern.
Aber das Prinzip ist ja das gleiche. Und irgendwo muss am Ende ja was rauskommen und das ist dann der Event mit einem Resultat. Den kann ich ja nicht weiter verdrahten - muss das Zeug ja nehmen und in die UI schmeissen.
Oder verstehe ich da was grundsätzlich falsch?

So in etwa sieht das jetzt mal aus in einem ganz kleinen Sample:

private void button1_Click(object sender, EventArgs e)
{
PackageReader reader = new PackageReader();
PackageValidator validator = new PackageValidator();
validator.OnContentValidated += b => MessageBox.Show(b.ToString());

PackageValidatorPlatine.Compose(reader, validator);

reader.ReadFile("myPackage.pac");
}

Ralf Westphal - One Man Think Tank hat gesagt…

@Lauring: Nein, nein, so würde ich das nicht machen.

1. Keine statische Methode fürs Verdrahten. Platinen sind Objekte.
2. Warum diese ad hoc Verdrahtung? Warum soll das Formular den Reader und den Validator kennen? Das halte ich für unsaubere Strukturierung.

Ich formuliere mal alternativ: Formular und Platine sind auf dem selben Abstraktionsniveau. Die Platine steckt einen Reader und einen Validator zusammen.

class YourForm : Form, IPortal
{
void button1_click(...)
{
this.Out_LoadFile("mypackage.pac");
}

public void In_ValidationResult(VResult b)
{
MessageBox.Show(b.ToString());
}
}

class PacValidatorBoard : IPacValidatorBoard
{
private IPackageReader rd;

public PacValidatorBoard(IPackageReader rd, IValidator val)
{
this.rd = rd;
val.In_Validate = rd.Out_PackageLoaded;
val.Out_Validated += b => this.Out_ValidationResult(b);
}

public void In_LoadFile(string filename)
{
this.rd.In_ReadFile(filename);
}

public Action Out_ValidationResult;
}

Das sind die beiden Komponenten auf Augenhöhe. Und darum gibt es noch ein MainBoard:

public class MainBoard : IWinFormsMainBoard
{
private IPortal portal;

public MainBoard(IPortal p, IPacValidationBoard pvb)
{
this.portal = p;
p.Out_LoadFile = pvb.In_LoadFile;
pvb.Out_ValidationResult = p.In_ValidationResult;
}

public void Run(string[] args)
{
Application.Run((Form)this.portal);
}
}

Und darum gibt es noch den Host:

static void Main(string[] args)
{
IUnityContainer uc = ...;
uc.RegisterType<IPortal, YourForm>(...); // als Singleton
uc.RegisterType<IPackageReader, PackageReader>();
uc.RegisterType<IValidator, PackageValidator>();
uc.RegisterType<IMainBoard, MainBoard>();

var mb = uc.Resolve<IMainBoard>();
mb.Run(args);
}

(Ich hoffe, das stimmt alles so; ich habs hier im Kommentarfeld einfach zusammenfabuliert.)

Siehst du die Ebenen?

1. Host - der nimmt das Mapping vor und baut letztlich durch Instanzierung der Root alles zusammen
2. MainBoard - verdrahtet auf der obersten Abstraktionsebene
3. YourForm und PacValidatorBoard - werden vom MainBoard zusammengestöpselt; sie stellen die nächste Abstraktionsebene dar
4. PacValidatorBoard - ist nochmal eine Platine mit zwei Bauteilen, dem Reader und dem Validator; die hätte aus Sicht der Form nicht Not getan, dient aber vielleicht dem Verständnis der Anwendung

Wichtig: Im Button findet keine Verdrahtung und keine Instanzierung statt. Da wären wir ja keinen Schritt weiter gegenüber früher.

-Ralf

Laurin Stoll hat gesagt…

Danke für die Erläuterungen Ralf.
Ich lasse das mal auf mich wirken und spiele damit noch ein wenig rum. Ja ist klar - sieht auch sinnig aus. Wirkt einfach nach ein wenig viel 'Overhead' - wobei hier die Frage wäre wie sich Overhead definiert. Lustig ja in der Softwareentwicklung, dass fast immer wenn man etwas gut strukturieren möchte man relativ viel Overhead hat. Pfuschen ist halt schneller....

Ich meld mich wieder - ist hochspannend - ich denke das kann ein Meilenstein in Sachen wirklicher Komponentenorientierung und behrrschbarer Komplexität werden. Ganz besonders daran fasziniert mich, dass es die Weichen für viele brisante Themen stellt (eines davon z.B. Parallel computing).

Ralf Westphal - One Man Think Tank hat gesagt…

@Laurin: Overhead... Das ist ein Begriff aus dem Effizienzdenken. Aus Sicht der Effizienz (Welche? Entwicklungsgeschwindigkeit oder Laufzeitperformance) ist jede Indirektion (also auch eine virtuelle Methode oder der Stack) ein Overhead.

Aber Effizienz ist eben nicht alles. Wir müssen auch verstehen und planen, was da was leisten soll. Deshalb brauchen wir noch was anderes, z.B. Flexibilität und Verständlichkeit.

Die bringen Abstraktionen und Indirektionen. Beispiel Methodensignatur. Die erzeugt Overhead im generierten Maschinencode. (Naja, heut nicht mehr, aber früher.)

Genauso nun EBCs. Ein Modell, mit dem sich einfacher planen lässt und das Flexibilität erzeugt. Aber bei der Übersetzung in Code kommt halt Overhead heraus.

Ist der schlimm? Ne. Von der Laufzeitperformance her sind EBCs kein spürbarer Nachteil. (Zumindest bis mir einer per Messung zeigt, dass eben durch EBCs etwas so langsam wird, dass der Kunde nicht damit leben kann.) Und von der Entwicklungsperformance her ist das nur so lange etwas nervig, solange ein gescheitest Tooling fehlt.

Dasselbe galt für DI ohne DI Container. Dasselbe galt für Parameterübergabe via Stack ohne Hochsprachen.

Also: Wenn wir die Abstraktion von EBCs mögen, dann sollten wir uns um Tools kümmern.

-Ralf