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.

Montag, 29. März 2010

Architekten programmieren nicht [OOP 2010]

Habe ich Ihre Aufmerksamkeit? Architekten programmieren nicht, ja das glaube ich – und stehe damit wohl gegen den (agilen?) Mainstream. Aber etwas “Gegenstrom” macht frisch und kregel ;-)

Wer etwas auf sich hält, sagt mal etwas zur Rolle des Architekten. Nach Lektüre eines weiteren Artikels von Neal Ford habe ich mir gedacht, darin sollte ich anderen nicht nachstehen. Also hier mal einige Gedanken zur Aufgabe des Architekten in der Softwareentwicklung.

Architekt als Rolle

Zunächst: Architekt ist für mich eine Rolle. Das ist ganz wichtig. Denn sonst wäre es unmöglich, dass sich kleinere Projekte einen Architekten leisten könnten. Idealerweise nimmt dann ein Projektbeteiligter (oder eine kleine Gruppe) die Rolle des Architekten ausschließlich ein – doch das ist eben nicht immer möglich. Also kann in kleinen Teams dieselbe Person sowohl Architekt wie noch etwas anderes sein.

Architekt als Schnittstellenfunktion

In Übereinstimmung mit vielen anderen bin ich sicher auch noch, wenn ich den Architekten als eine Person an der Schnittstelle zwischen anderen Projektbeteiligten sehe. Der Architekt ist insofern ein Übersetzer. Er hat Kontakt mit dem Kunden. Er muss mit der Projektleitung auf Augenhöhe sprechen. Er ist ein wichtiger technischer Ansprechpartner für das Entwicklerteam.

image

Ein Architekt muss daher ein gerüttelt Maß an Softskills besitzen. Er ist in ständigen Gesprächen und Verhandlungen. Er muss immer wieder auf dem Verständnisniveau seiner Gesprächspartner erklären. Er muss gut zuhören können. Er muss vermitteln/moderieren können. Und er braucht auch Durchsetzungskraft.

Architektur von Systemen von Funktionseinheiten

Ein Architekt hat aber natürlich nicht nur mit Menschen zu tun. Sein Metier ist die Software. Und da dreht sich seine Verantwortung ganz allgemein darum, Systeme von Funktionseinheiten zu entwerfen. Ja, genau so allgemeine denke ich das. Eine Funktionseinheit kann ein Betriebssystem Prozess (oder EXE) sein, das kann eine Maschine sein oder eine ganze Anwendung oder ein Cluster. In jedem Fall geht es um “Bausteine”, die Software sind oder Software ermöglichen, die zusammengeschaltet einen Kundennutzen bieten.

So allgemein möchte ich die Definition halten, um der ewigen Diskussion, was denn der Unterschied zwischen Application Architect und Enterprise Architect usw. sei, aus dem Weg zu gehen. Das sind einfach alles Architekten, die mit dem Entwurf von Funktionseinheiten zu tun haben, die nur eben auf unterschiedlichen Abstraktionsebenen liegen. Die Prinzipien sind jedoch auf allen Ebenen eher mehr als weniger gleich. Flexibilität und Effizienz müssen auch immer in Balance gehalten werden.

Und wenn Sie daraus jetzt lesen, dass dann doch jeder Entwickler auch Architekt sei, weil er Funktionseinheiten plane (allemal Klassen und Methoden), dann haben Sie Recht. Jeder Entwickler ist auch immer auf einer gewissen Ebene Architekt. (Manche nennen dann das, was er plant, Design statt Architektur, aber den Unterschied möchte ich grad mal vernachlässigen.)

Architektur als Abstraktion

Architekten gehen mit Funktionseinheiten um, Entwickler auch. Wo ist dann der Unterschied zwischen beiden?

Architekten planen, Entwickler bauen.

Das ist der entscheidende Unterschied. Er liegt wieder mal im Abstraktionsniveau. Architekten haben Überblick, Entwickler Detailblick. Architekten stehen auf einem Hügel und entwickeln Strategien, Entwickler führen aus.

Eine solche Aufgabenteilung halte ich für absolut natürlich und naheliegend. Führung und Ausführung sind zwei ganz unterschiedliche Tätigkeiten. Sozusagen unterschiedliche concerns der Softwareentwicklung.

Ein Dirigent führt ein Orchester, ein Musik spielt darin ein Instrument. Das heißt nicht, dass ein Dirigent kein Instrument spielen müsse. Im Gegenteil! Dirigenten sind immer ausgewiesene Instrumentalisten sogar auf mehreren Instrumenten. Aber während ein Dirigent dirigiert, spielt er (gewöhnlich) keines seiner Instrumente.

Daraus leite ich auch ab, dass ein Architekt eben nicht programmieren sollte. Zumindest nicht in dem Projekt, für das er Architekt ist. Das wäre ein Widerspruch zum Prinzip der Separation of Concerns, das auch auf Organisationen angewandt werden sollte.

Ein Architekt soll technisch absolut fit sein. Keine Frage! Er muss sich fit halten, er muss dem Team, das seine Architektur technisch auf Augenhöhe begegnen, er muss Erfahrung im Einsatz von Technologien haben, damit er sie planen kann. Dazu braucht es Praxis, Praxis, Praxis. Aber diese Praxis sollte ein Architekt sich nicht (!) in dem Projekt holen, in dem er Architekt ist.

Er mag im Pair Programming mit Entwicklern arbeiten – aber nicht als Driver. Er  mag Spikes entwickeln, um eine Architekturvision abzuklopfen. Er soll natürlich in Reviews eine sehr aktive Rolle spielen. Aber von Produktionscode sollte er die Hände lassen.

Der Grund ist ganz einfach: Wenn der Architekt seine Hände im Produktionscode schmutzig macht, gewinnt er soviel Detailwissen, verstrickt er sich so sehr im Tagesgeschäft, dass er seine Aufgabe als derjenige, der Überblick behält, nicht mehr erfüllen kann. Denn Überblick, Weitblick, Strategie: darum geht es bei Architektur. Bezogen auf die Abstraktionsebene soll der Architekt die Interessen des Großen Ganzen vertreten. Dazu braucht es jedoch Distanz von den nitty gritty details.

Architekt als Bewahrer

Und was ist das Große Ganze, für das der Architekt verantwortlich ist? Was ist das Ergebnis seiner Schnittstellenfunktion? Er grenzt sich zunächst von Projektmanagement oder auch einem ScrumMaster und ProductOwner ab, die auch das Große Ganze im Blick haben. Aber ganz untechnisch. Deren Interesse ist es, dass es voran geht. Die wollen nicht unter die Haube der Benutzeroberfläche einer Anwendung schauen. “Muss laufen!” ist ihr Credo. (Wobei der ScrumMaster sogar noch weniger Anspruch hat. Ihm geht es nur darum, dass das Vorgehensmodell korrekt gelebt wird. Wohin damit dann gegangen wird, ist ihm eigentlich egal.)

Der Architekt ist also der technische “Großverantwortliche”. Er muss den Einsatz von Technik und technischen Konzepten so planen, dass sie die Kundenwünsche erfüllt. Er wie die anderen ist er dabei selbstverständlich dem Zeit- und Kostenrahmen verpflichtet.

Konkret ist die Aufgabe des Architekten, dass die folgenden Aspekte von Software im Sinne des Kunden ausgeprägt und in Balance sind:

  • Nützlichkeit
  • Benutzbarkeit
  • Belastbarkeit
  • Evolvierbarkeit

Zu dieser Liste hat mich die Alte Definition von (Bau)Architektur in Wikipedia inspiriert. Nach der beruht Architektur auf drei Prinzipien: Stabilität, Nützlichkeit und Anmut (firmitas, utilitas, venustas).

Software ist kein Bauwerk. Also geht es bei Softwarearchitektur auch um anderes als Bauarchitektur. Dennoch glaube ich daran, dass Softwarearchitektur genauso scharf definiert werden sollte. Diese Aspekte scheinen mir das so zu leisten, dass keiner wegfallen darf und keiner mehr dazu kommen muss. (Oder habe ich eine –keit übersehen?)

Nützlichkeit: Software hat nur einen Zweck: Dem Kunden zu nutzen nach seinen Anforderungen. Das technisch sicherzustellen, ist die vornehmste Aufgabe des Architekten. Software die nicht nutzt, ist ihr Geld nicht wert. Nützlichkeit ist hergestellt, wenn die funktionalen Anforderungen wie auch nicht funktionale Anforderungen erfüllt sind. Nur wenn die Software tut, was sie tun soll, wenn sie das schnell genug tut und sicher genug usw., dann nützt sie. Darauf ist das System der Funktionseinheiten und die Nutzung von Technologien auszurichten.

Benutzbarkeit: Alle Nützlichkeit nützt nichts, wenn sie dem Anwender nur schwer zugänglich oder verständlich ist. Den Architekten sehe ich daher – vielleicht etwas untypisch – in der Verantwortung, die Usability im Blick zu behalten. Er muss das Team immer wieder dazu anhalten, Funktionalität nicht nur irgendwie auf die Straße zu bringen, sondern in einer Weise, dass sie dem Anwender auch “ohne Verrenkungen” zur Verfügung steht. Dem Kunden (ProductOwner) kann es nicht überlassen werden, schon bei der Anforderungsdarlegung oder durch Nörgeln beim Test von Releases Usability einzufordern. Die Softwareentwicklung selbst muss davon eine Vision haben – und die vertritt der Architekt.

Belastbarkeit: Software läuft nicht immer nur in Schönwetter. Benutzerzahlen steigen ungeplant, Datenvolumina nehmen unerwartet zu, Angreifer suchen nach Schlupflöchern, Anwender gehen unsachgemäß mit ihr um, Hardware fällt aus… Software ist vielen Belastungen diesseits und jenseits expliziter Kundenanforderungen ausgesetzt. Der Architekt muss das voraussehen und seine Planung angemessen darauf abstellen. Er  muss Toleranzen “einrechnen” und Spielräume schaffen. Dazu ist natürlich Erfahrung nötig – aber keine Glaskugel ;-) Belastbarkeit bedeutet, dass Software ihre Nützlichkeit auch über die unmittelbaren Anforderungen hinaus behält oder mit wenig Aufwand wiedergewinnen kann.

Evolierbarkeit: Zuguterletzt ist der Architekt dafür zuständig, über die Anforderungen des Kunden heute hinaus zu sehen. Zumindest er muss das Verständnis haben, dass Software sich ständig wandelt und daher eine Struktur haben muss, die sich an solchen Wandel immer wieder leicht anpassen lässt. Ein Architekt kennt den Kunden insofern besser als der sich selbst ;-) Auch wenn der Kunde meint, alle Wünsche geäußert zu haben, weiß der Architekt, dass das hochwahrscheinlich nicht der Fall ist – und muss seine Architektur (angemessen) flexibel halten. Software soll dem Kunden nicht nur heute dienen, sondern auch morgen und übermorgen. Evolvierbarkeit ist die Bedingung für die Möglichkeit zukünftiger Nützlichkeit.

Zur Evolvierbarkeit gehört auch, dass Veränderungen nicht nur möglich sind, sondern das auch mit vertretbarem Aufwand. Allemal hier schwingt also auch die Produktionseffizienz als Aspekt mit. Speziell herausgehoben habe ich sie jedoch nicht, da sie ansonsten der allgemeinen Verantwortung des Architekten unterliegt, der – wie die anderen Projektverantwortlichen auch – darauf achtet, dass mit Zeit und Geld sorgsam umgegangen wird.

Und ein Wort noch zur Korrektheit: Nützlichkeit schließt Korrektheit ein. Deshalb habe ich die Korrektheit nicht in dieser Verantwortlichkeitsliste ausdrücklich aufgeführt. Sie ist so grundlegend, dass sie eigentlich keiner Erwähnung bedarf. Ohne die Möglichkeit, Korrektheit jeder Funktionseinheit “auf Zuruf” belegen (oder zumindest überprüfen) zu können, ist das Ergebnis der Planung eines Architekten nicht wirklich Architektur zu nennen. Nützlichkeit und Evolvierbarkeit sind ohne Korrektheit bzw. ihre ständige systematische und damit automatisierte Prüfung nicht denkbar.

Der Architekt plant eine Software, d.h. ein System aus Funktionseinheiten, so, dass die Aspekte Nützlichkeit, Benutzbarkeit, Belastbarkeit und Evolvierbarkeit im Sinne absehbarer Kundenanforderungen ausbalanciert sind. Das ist keine leichte Aufgabe. Sie erfordert viel Erfahrung und technisches Know-How.

Vor allem erfordert sie aber eben ein strategisches Denken, das sich nicht gemein machen darf mit Codierungsdetails. Deshalb glaube ich ganz fest, dass ein Architekt nicht in seinem Architekturprojekt programmieren sollte. Er verliert sonst die Distanz, die nötig ist, um sich für diese vier wichtigen Aspekte gegen den Druck zu stemmen, den unweigerlich Projektmanagement, Kunde und Team auf ihn ausüben.

Mittwoch, 24. März 2010

Call for Papers: prio.conference – Verteilte Architektur

Am 19. und 20. Oktober 2010 ist es wieder soweit: Die prio.conference (www.prioconference.de) widmet sich einem Thema, das viele Entwickler bewegt. In diesem Jahr ist das “Verteilte Architektur”.

image

Wie in den Vorjahren bin ich Technical Chair und suche spannende, wegweisende, lehrreiche Vorträge. Wer seine Erfahrung zum Thema “Verteilte Architektur” weitergeben möchte oder jemanden kennt, der etwas zu sagen hat, der melde sich per Email an techchair (at) prioconference.de.

Der offizielle Call for Papers ist hier: http://www.prioconference.de/Call-for-Papers

Ich bin gespannt auf Ihre Einreichungen…

Montag, 22. März 2010

TDD, aber bitte mit System

Hier sind zwei recht ordentliche Artikel zum Thema TDD: Teil 1 und Teil 2. Mir gefällt daran besonders, dass Neal Ford sein TDD beginnt mit einer kleinen Liste von Schritten auf dem Weg zur Beantwortung der Frage, ob eine Zahl eine Vollkommene Zahl ist.

Bei allem Gefallen an seinen Artikeln reibe ich mich jedoch an einigen Punkten.

Fragliche Objektorientierung

Wenn Sie die Wurzel einer Zahl berechnen wollten und ich würde Ihnen sagen, das geht mit C# so:

var sqc = new SqrtCalculator(7);
Console.WriteLine(“Wurzel aus 7: {0}”, sqc.Calculate());

Würden Sie das als naheliegend empfinden? Kaum.

Oder wenn ich Ihnen anbieten würde, einen String bei einer Zeichenkette so zu splitten:

var splitter = new StringSplitter(“ ”);
var words = splitter.Split(“the quick brown fox”);

Würden sie das als komfortable ansehen? Kaum.

Nichts anderes schlägt Neal Ford aber vor, wenn er seine Implementation zur Prüfung von Zahlen auf Vollkommenheit so aussehen lässt:

var c = new Classifier(6);
Console.WriteLine(“Ist 6 vollkommen: {0}”, c.IsPerfect());

Das ist genausowenig komfortabel oder naheliegend wie die obigen Beispiele. Das ist erzwungen objektorientiert. Das ist ein für den Anwender der Funktionalität nicht nachvollziehbarer API.

Die Prüfung, ob eine Zahl vollkommen ist, ist eine Funktion genauso wie die Prüfung, ob eine Zahl gerade ist oder die Berechnung ihrer Wurzel. Warum also sollte ich als Nutzer einer solchen Funktion(alität) eine Klasse instanzieren müssen? Warum sollte diese Klasse auch noch zustandsbehaftet sein?

Neil leitet das auf Seite 8 seines Teil 1 daraus ab, dass zwei interne Funktionen (factorsFor(int number) und isFactor(int number)) auf demselben Parameter arbeiten, der zu prüfenden Zahl. Er meint, das sei nun wirklich zuviel prozedurale Programmierung. Oder vielleicht scheut er sich vor der “Feuchtigkeit” der Wiederholung eines Parameters? Ich weiß es nicht. Ich finde es einfach nur überkandidelt – und zwar leider in einer Weise die symptomatisch ist für die Branche. Denn sein Vorgehen ist ja tragisch: Er will das Gute erreichen, verschlimmert die Situation aber. Er möchte einem Paradigma dienen, das für sich gepachtet zu haben scheint, dass es durch und durch gut sei – aber das Paradigma passt leider nicht zum Problem. Mit dem Hammer Objektorientierung wird die funktionale Schraube eingeschlagen. Autsch!

Hier dagegen meine Lösung:

public class VollkommeneZahlen

{

    public static bool IstVollkommen(int zahl)

    {

        return EchteTeilerVon(zahl)

              .ErgebenEineVollkommeneZahl(zahl);

    }

    …

Die Nutzung sieht so aus:

Console.WriteLine(VollkommeneZahlen.IstVollkommen(6));

Das halte ich sowohl in der Anwendung wie auch in der Implementation für intuitiv und angemessen. Egal, ob das nun speziell objektorientiert oder sonstwas ist. Der Anwender versteht es, weil eine Funktion als Funktion realisiert ist. Und der, der den Code liest, sieht sofort, wie das Ergebnis in zwei Schritten zustande kommt.

Unklare Verteilung von Verantwortung

Nicht nur ist aber die Objektorientierung für das Problem unangemessen, wie ich finde. Sie hat sogar einen negativen Effekt auf die Verständlichkeit und Flexibilität des Codes.

Vollkommene Zahlen sind solche, deren echte Teiler in Summe die Zahl ergeben. Die Teiler von 6 sind 1, 2, 3 und 6. Allerdings ist 6 kein echter Teiler. Die Summe ist deshalb nur aus 1, 2 und 3 zu bilden und die ergibt 6. Damit ist 6 eine Vollkommene Zahl.

In Neal Fords Code ist die besondere Behandlung der Zahl selbst als unechter Teiler an zwei Stellen codiert:

image

Er fügt die Zahl selbst als Teiler zunächst der Menge aller Teiler hinzu – und zieht sie am Ende wieder ab. Damit erfüllt auch seine Funktion calculateFactors() nicht mehr wirklich ihre Verantwortlichkeit, die Teiler der Zahl zu berechnen. Das trägt nicht zur Verständlichkeit der Algorithmusimplementierung bei.

Außerdem ist isPerfect() nicht nur von der Zahl als Zustand abhängig, sondern auch noch indirekt von der Menge der Teiler, die calculateFactors() berechnet und sumOfFactors() auswertet. Solcher Zustand erhöht immer die Komplexität einer Lösung. Und er macht die Wiederverwendung von Methoden in anderen Zusammenhängen schwieriger.

Meine Funktion IEnumerable<int> EchteTeilerVon(int zahl) hingegen ist von nichts abhängig und kann daher in jedem Zusammenhang, in dem nur die echten Teiler einer Zahl relevant sind, wiederverwendet werden.

Auch macht bool ErgebenEineVollkomeneZahl(this IEnumerable<int> teiler, int zahl) den Code selbstdokumentierender. Das Abstraktionsniveau der Bestandteile von IstVollkommen() ist einheitlich. Bei Neal hingegen liegen calculateFactors() und der Ausdruck zur Berechnung des Rückgabewertes auf unterschiedlichen Abstraktionsniveaus. Der Leser muss sich zusammenreimen, dass eine Zahl vollkommen ist, wenn die Summe von Teilern abzüglich einer Zahl gleich der Zahl ist. Zugegeben, das ist nicht so kompliziert, doch es ist ein spürbarer intellektueller Aufwand, der nicht Not tut. (Insbesondere verwunderlich ist sein Ansatz, da er in einem anderen Artikel das Single Level of Abstraction Prinzip lobt.)

Gesucht: Klares Vorgehen

Schließlich vermisse ich bei Neals Artikeln, dass er sein Vorgehen nicht weiter deutlich sichtbar systematisiert. Er tut schon das Richtige, in dem nicht mit TDD “reinspringt” und als erstes einen Test für isPerfect() schreibt, sondern mal einen Gedanken an den Algorithmus verschwendet. Er denkt also nach, bevor er codiert. Doch das hebt er nicht hervor. Einzig der “TDD workflow” wird als Handlauf für das Vorgehen wieder einmal thematisiert.

Schade. Denn zu TDD gehört mehr, denke ich. Hier mein Version eines Entwicklungsprozesses, in den TDD eingebettet sein sollte:

  1. Verstehen
  2. Nachdenken/planen
  3. Unsicherheiten ausräumen
  4. Codieren

Diese Schritte beziehen sich auf ein Problem bzw. auf die das Problem lösende Funktionseinheit. Sie sind daher rekursiv zu durchlaufen, sollte die Funktionseinheit in weitere zerfallen. Das ist ein wichtiger Punkt! Denn auf Zerlegungsebene eines Problem gilt es zuerst zu verstehen, dann zu planen usw.

Zu 1: Am Anfang der Entwicklung einer Softwarelösung – sei es Methode oder Klasse oder Komponente oder Anwendung – steht das Verstehen. Investieren Sie Zeit, das Problem oder gar den Kunden zu verstehen. Machen Sie sich klar, was die Anforderungen wirklich sind. Tragen Sie Fälle zusammen, die beschreiben, was die Eingaben/Parameter und der zugehörigen erwarteten Ausgaben/Ergebnisse sind. Das hört sich selbstverständlich an. In der Praxis ist aber schwierig und bedarf immer wieder Mut und Disziplin. Beide werden oft nicht ausgebracht. Schöner ist es, schnell den Code Colt zu ziehen und zu programmieren.

Leider ist ungenügendes Verständnis aber wohl eine der wesentlichen Ursachen für soviele Probleme der Softwareentwicklung. Wer ungenügend versteht, der implementiert zuviel. Wer ungenügend versteht, der implementiert das falsche oder inkorrekt.

Letztlich ist ungenügendes Verständnis zwar nie ganz zu vermeiden, doch ein wenig mehr Mühe darf es schon sein. Vor allem ist in den Prozess des Verständnisaufbaus der Kunde sehr aktiv mit einzubeziehen. Fragen Sie ihm Löcher in den Bauch! Dafür ist er da ;-) Lassen Sie ihn nicht aus seiner Verantwortung. Wenn er etwas von Ihnen will, dann soll er wirklich so genau wie möglich beschreiben, was das ist, wie es aussehen soll, wie es sich verhalten soll. Erbitten Sie von ihm sehr konkrete Abnahmetests.

Falls der Kunde sich bei solch “peinlichen Befragung” ziehrt, machen Sie ihm klar, dass das Ergebnis Ihrer Arbeit nur so gut sein kann wie seine Spezifikation. Garbage in, garbage out – das gilt auch hier. Wer nur ungenau spezifiziert, der kann auch nur ein ungenaues Ergebnis bekommen. Dann muss nachgebessert werden. Das macht niemandem Freude.

Da die meisten Kunden mit sehr genauer Spezifikation überfordert sein werden, gibt Ihnen das die Chance, ein agiles Vorgehensmodell “zu verkaufen”. Nutzen Sie die Chance! Aber nehmen Sie das nicht als Entschuldigung, ohne gründliches Verständnis mit dem Codieren zu beginnen.

Zu 2: Wenn Sie meinen, das Problem durchdrungen zu haben, sollten Sie noch nicht zur Tastatur greifen. Auch nicht, wenn Sie TDD betreiben wollen. Tun Sie sich den Gefallen und denken Sie zuerst über einen Lösungsansatz nach. Diese Phase fällt leider immer wieder zu kurz aus. Auch in wohlmeinenden Coding Dojos wird das nicht unbedingt praktiziert. TDD als Test-Driven Design verstanden soll es richten. Codieren kann Planung und Nachdenken aber nicht ersetzen, sondern allenfalls unterstützen. Eine Struktur für Ihre Software ergibt sich nicht einfach. Die will bewusst entworfen werden.

Das ist, was mir an Neals Artikel gefallen hat: Er hat über das Problem nachgedacht und einen kleinen Plan aufgestellt. Er ist auf zumindest drei Funktionseinheiten/Verantwortlichkeiten gekommen, aus denen eine Lösung für das Problem “Vollkommene Zahlen erkennen” besteht:

  • Potenzielle Teiler einer Zahl erzeugen
  • Feststellen, ob eine potenzieller Teiler tatsächlich ein Teiler ist
  • Aufsummieren der tatsächlichen Teiler, um festzustellen, ob sie die Zahl ergeben

Das sind zugegeben kleine Funktionseinheiten. Macht aber nichts. Jede, die Sie beim Nachdenken finden, ist eine gute. Denn damit bekommen Sie “Bausteine” an die Hand, die sie separat testen können. Das ist immer gut. Und Sie können womöglich entscheiden, in welcher Reihenfolge Sie deren Umsetzung angehen.

Ein zunächst monolithisches Problem zerfällt so in kleinere Probleme und die womöglich wiederum in kleinere usw. Nachdenken hilft also bei der Komplexitätsbewältigung.

Zu 3: Allerdings mag es sich herausstellen, dass Sie sich mit der Umsetzung der einen oder anderen Funktionseinheit, auf die Sie beim Nachdenken gestoßen sind, nicht 100%ig wohlfühlen. Das ist ganz normal. Es mag an der Problemdomäne liegen oder an einer Technologie, die zum Einsatz kommen soll. Deshalb ist es wichtig, dass Sie vor dem Codieren noch einen Zwischenschritt machen.

Seien Sie sensibel für Ihre Unsicherheiten und räumen Sie sie aus. Das können Sie durch das Studium von Fachliteratur tun. Oder Sie befragen den Kunden nochmal. Oder Sie programmieren etwas. Rotzen Sie Code raus (Spike Solution), um sich z.B. mit dem neuen O/R Mapper vertraut zu machen, bevor Sie damit Produktionscode schreiben. Oder für das Problem der vollkommenen Zahlen könnten Sie sich Erweiterungsmethoden anschauen, die es möglich machen, die Funktion IstVollkommen() so lesbar zu gestalten.

Solange Sie noch unsicher sind, sollten Sie nicht mit der Codierung beginnen. Ansonsten entsteht schnell akzidenzielle Komplexität, d.h. Komplexität, die nicht nötig ist, die Sie letztlich auch nicht wollen.

Wenn Sie etwas programmieren, um Ihre Unsicherheit abzubauen, dann schmeißen Sie es am Ende besser weg. Nehmen Sie Erkenntnisse mit ins Codieren, aber keinen Code. Auch sind solche Spike Solutions keine Prototypen. (Allerdings können Sie offizielle Prototypen natürlich immer noch mit dem Kunden vereinbaren. Dann teilen Sie Ihre Unsicherheit mit dem Kunden.)

Zu 4: Erst wenn Sie genau verstehen, was der Kunde braucht, einen Plan haben, wie Sie ihm das geben können, nicht mehr unsicher sind, erst dann sollten Sie mit dem Codieren beginnen. Das gilt aus meiner Sicht auch für TDD. Oder gerade für TDD! Denn immer wieder sehe ich bei Entwicklern, die mit TDD beginnen, dass sie Schwierigkeiten haben, sich zuerst Tests vorzustellen, bevor sie etwas implementiert haben.

Das liegt meiner Meinung nach daran, dass sie das Problem noch nicht genügend gut durchdrungen haben bzw. der Kunde keine Akzeptanztestfälle geliefert hat. Und/oder es liegt daran, dass sie unsicher sind, was eigentlich zu implementieren ist. Denn wer davon keine Vorstellung hat, der tut sich schwer damit, Erwartungen zu formulieren.

Der Effekt ist dann oft, dass TDD mit trivialen Tests oder Sonderfalltests begonnen wird. Die für den Kunden viel wichtigeren “happy day” Szenarien werden dadurch auf die Lange Bank geschoben. Klarheit für deren Implementation ergibt sich auch nicht aus solchen Tests. Geschäftig mit Tests zu beginnen kann also auch ein subtiles Symptom von Prokrastination sein.

Zusammenfassung

TDD ist eine zentrale Praktik für professionelle Softwareentwicklung. Gerade deshalb ist es nötig, sich ihr ganz bewusst zu sein und immer wieder zu fragen, ob sie schon optimal betrieben wird. Ein systematisches Vorgehen hilft dabei – gerade am Anfang.

Vor dem falschen Programmierparadigma schützt TDD aber natürlich nicht. Wir tun also gut daran, auch seine Grenzen bewusst zu sehen. TDD produziert also immer nur Code auf dem fachlichen Wissensstand seines Anwenders. Deshalb ist es gut, immer wieder über den Tellerrand (der Objektorientierung) zu schauen und sich mit anderen auszutauschen.

Sonntag, 21. März 2010

Generiert – Architekturcompiler für Event-Based Components

Da hab ich ja was angerichtet: Event-based Components ziehen immer größere Kreise. Immer mehr Entwickler “wollen es tun”. Selten habe ich soviel direktes positives Feedback zu einem Konzept bekommen. Irgendwie scheine ich da einen Nerv getroffen zu haben.

Bei allem Wohlwollen haben EBCs aber natürlich auch noch Schwachstellen. Die kommen durch das Feedback und das “Herumspielen” mit dem Konzept ans Licht. Eines ist die Verdrahtung. Die ist zwar technisch simpel, aber nervig zu implementieren. Weil nicht nur wenige Komponenteninstanzen in andere zu injizieren sind, sondern viele Pins der Komponenteninstanzen mit denen anderer verbunden werden müssen, steigt der Aufwand für die Laufzeitintegration. Dem stehen zwar große Gewinne gegenüber – doch nervig bleibt die Verdrahtung.

Besser würde die Lage natürlich, gäbe es einen hübschen EBC Designer à la LabVIEW.

image

Soweit sind wir aber noch nicht. Das braucht noch etwas mehr Erfahrung mit EBCs, glaube ich. Und auch Zeit, denn einen Designer zu basteln, ist kein Pappenstil.

Geht´s denn nicht aber auch ohne Designer schon ein bisschen einfacher? Einen “automatischen Verdrahter” hatte ich ja schonmal gebastelt. Der funktioniert aber nur in sehr einfachen Szenarien ausreichend. Wenn es komplizierter wird, reicht “Verdrahtung nach Konvention” nicht mehr aus. Dann müssten zusätzliche Informationen her. Dann wären wir wieder bei einer Architekturbeschreibung.

Also habe ich mir gedacht: Warum nicht mit einer Architekturbeschreibung ohne Designer anfangen? Also habe ich mal “rumgesponnen”. Erst habe ich mir ne textuelle DSL überlegt. Doch dann dachte ich, dass es noch besser wäre, noch klarer zu entkoppeln. Also bin ich auf das gute alte XML gekommen.

Wie wir am Ende Architekturen entwerfen und formulieren, ist egal. Eine graphische oder auch textuelle Notation wird sich immer in ein XML-Format übersetzen lassen. Das kann die Konstante in der Mitte sein zwischen Entwickler und Code.

Deshalb habe ich mir mal ein ganz einfaches Format überlegt, mit dem man einfache EBC-Architekturen beschreiben kann. Als Beispiel eine Stopuhr-Anwendung. Zuerst die Architektur gemalt in Visio:

image

Und hier die Übersetzung in das XML-Format. Ich nenne es mal ebclang für EBC Language:

image

Die Komponenten sind für sich mit ihren Output- und Input-Pins beschrieben, anschließend die Verdrahtung. Ich denke, das ist recht einfach zu verstehen. Geschachtelte Komponenten habe ich einstweilen allerdings noch außen vor gelassen.

Nachdem ich so eine XML-Architekturbeschreibung hatte, habe ich die von Hand ganz systematisch in Codeartefakte übersetzt. Das funktionierte sehr einfach. Es lässt sich gut automatisieren.

Deshalb habe ich in einem Anfall von Lust am “mud wrestling” ;-) einen Übersetzer dafür gebastelt. Der ist aus dem Stand gleich zu sehr hübschem Brownfield Code geworden. Aber das macht nichts. Er ist als Spike Solution gedacht, nicht mehr. Ich wollte mit dem Code nur das Feld der Artefaktgenerierung explorieren, bevor ich eine “richtige” Version entwickle.

Der ebclang.compiler übersetzt eine XML-Architekturdefinition wie oben in Assemblies und Visual Studio Projekte. Das sind im Einzelnen:

  • myapp.messages.dll und myapp.messages.csproj: Ein Projekt, das alle Nachrichtentypen implementiert. Der Compiler legt jeden Nachrichtentyp, der kein Standardtyp ist, darin als Klasse an.
  • myapp.specifications.dll: Eine Assembly, die die Komponenten in Form von Interfaces spezifiziert.
  • myapp.wiring.dll: Eine Assembly, die Komponenteninstanzen verdrahtet.

“myapp” steht für den Applikationsnamen wie im XML auf <MainBoard> angegeben. Die Spezifikation und das Wiring werden bewusst nur als Assemblies erzeugt, damit niemand auf die Idee kommt, sie zu verändern. Darin steckt die manifestierte Architektur. Die soll nur durch das XML veränderbar sein.

Die Nachrichtentypen jedoch müssen “nachgebessert” werden können. Deshalb darf man am messages-Projekt arbeiten. Der Compiler überschreibt Nachrichtentypquelldateien nicht.

Zusätzlich legt der Compiler noch Werkbänke für die Komponenten an. Das sind VS Projektmappen mit zwei Projekten, einem für die Komponentenimplementation und einem für deren Tests.

Das Vorgehen beim Entwurf einer EBC-Architektur ist mit dem Compiler wie folgt:

1. Anlegen eines Verzeichnisbaums für die Artefakte. Hier die Minimalstruktur:

image

Wurzelverzeichnis, darunter ein Verzeichnis lib/ mit nunit.framework.dll und weiteren für die Anwendung nötigen Bibliotheken. Und das Verzeichnis arc/, in dem die Architekturbeschreibung liegt. (Das Batch-File ruft den ebclang-Compiler für die XML-Datei auf.)

Nach Übersetzung der Architekturdefinition sieht der Artefaktbaum so aus:

image

Die .log-Dateien enthalten den C#-Compiler Output der Übersetzungen der myapp.* Projekte. Falls mal was schief gegangen sein sollte, darin nachschauen, bei welchem Generierungsschritt es gehakt hat. Der Compiler ist eine Konsolenanwendung und liefert auch noch Informationen während der Übersetzung.

Die Projekte *.specifications und *.wiring sollten nicht weiter beachtet werden. Der Compiler könnte sie genauso gut löschen. Ich habe sie bisher zur Fehlersuche aber noch drin gelassen. *.messagetypes enthält das Projekt, in dem die Nachrichtentypen “ausgefleischt” werden können.

Wichtig ist das bin-Verzeichnis in arc/:

image

Hier sind die Assemblies angekommen, die der Compiler erzeugt hat. Von hier müssen sie in allen weiteren Anwendungsprojekten referenziert werden. ebclang.basics.dll enthält bisher nur einen Nachrichtentypen für bidirektionale Kommunikation (Request<,>) und seine Erweiterungsmethoden.

Mehr ist eigentlich nicht nötig als Vorarbeit durch den Compiler. Auf der Basis der Assemblies kann man loslegen mit der Anwendungsimplementation. Die aufwändige Verdrahtung steckt ja in *.wiring.dll. Das EBC-Leben ist damit einfacher geworden.

Dennoch tut der Compiler etwas mehr. Er erzeugt auch noch Gerüstprojektmappen für die Komponenten im source/ Verzeichnis des Artefaktbaumes:

image

Diese Werkbänke geben den Rahmen vor, in dem Komponenten implementiert werden sollen. Erstens fokussieren sie den Blick, indem sie jede Komponente in eine eigene Projektmappe stellen. Zweitens geben sie vor, dass automatisiert getestet werden muss. Drittens setzen sie schon die Referenzen auf die generierten Assemblies und stellen den Output-Path auf ein globales bin-Verzeichnis.

Das mögen Kleinigkeiten sein. Doch in Clean Code Developer Seminaren, die viel mit Architektur zu tun haben, bemerken wir immer wieder, dass diese Kleinigkeiten viel Zeit kosten. Also habe ich versucht, hier eine erste Linderung zu bringen. Die ist noch nicht perfekt, weil z.B. keine Klasse für die Komponentenimplementation generiert wird, aber sie ist ein Anfang.

Was bleibt ist die Visualisierung von Architekturdefinitionen. Die soll ultimativ natürlich ein Designer übernehmen. Bis dahin wollte ich aber nicht warten. Also habe ich noch einen kleinen Visualisierer für die XML-Definitionen gebastelt. Den macht man einfach parallel zu Visual Studio oder einem anderen XML-Editor auf und lädt die Architekturdatei. Immer, wenn die sich verändert, aktualisiert man die Diagrammdarstellung und bekommt visuelles Feedback.

image

Die Visualisierung ist natürlich nicht interaktiv und auch nicht besonders hübsch. Aber sie erfüllt erstmal ihren Zweck, denke ich. Und der ist, dass – wer will – einen leichteren Einstieg in den Umgang mit Event-based Components bekommt.

Am Flipchart mit einer Architekturskizze beginnen, die in das XML-Format übersetzen und dabei die Übersetzung mit dem Visualisierer immer wieder überprüfen. Am Ende die Artefakte mit dem Compiler erzeugen und Nachrichtentypen sowie die Komponentenimplementationen in Visual Studio entwickeln.

Fehlt am Ende nur noch eines: Ein Programm, dass die ganzen Komponenten instanziert und die generierte Verdrahtung aufruft. So ein Programm – ich nenne es Host – generiert der Compiler noch nicht. Aber es ist schnell geschrieben. Dem Compiler-Download liegt es in der Nachher-Version der Beispielanwendung bei:

image

Mit einem DI Container wie Unity sieht ein Host z.B. so aus:

   19 static void Main()

   20 {

   21     Application.EnableVisualStyles();

   22     Application.SetCompatibleTextRenderingDefault(false);

   23 

   24     // Prepare Build

   25     IUnityContainer uc = new UnityContainer();

   26     uc.RegisterType<IPortal, FrmPortal>(new ContainerControlledLifetimeManager());

   27     uc.RegisterType<IStopuhr, Stopuhr>();

   28     uc.RegisterType<MainBoard, MainBoard>();

   29 

   30     // Build & Bind

   31     var mainboard = uc.Resolve<MainBoard>();

   32 

   33     // Run

   34     Application.Run((Form)uc.Resolve<IPortal>());

   35 }

Das Portal ist ein Singleton, damit es nicht zweimal instanziert wird. Einmal während der Injektion in das MainBoard und einmal am Schluss bei Run().

Wer diesen ersten Wurf für einen Architekturcompiler für EBCs interessant findet, der kann ihn hier herunterladen. Wie immer freue ich mich über Feedback.

Freitag, 26. Februar 2010

Bindungsenergie – Gedanken über ein Tooling für Event-Based Components

Der Gewinn, den Event-Based Components (EBC) bringen, steht nach meinem vorhergehenden Blogartikel inzwischen unzweifelhaft fest. Bevor EBC weitere Kreise ziehen können, muss es dafür aber ein Tooling geben, glaube ich. Dependency Injection kann man von Hand betreiben – aber das macht keinen Spaß, wenn die Anwendungen größer werden. Dasselbe gilt sogar potenziert für die Verdrahtung von EBC “Pins”. Deren Zahl ist viel größer als die der Interfaces, für die DI zuständig ist.

Wie könnte also ein Unterstützung von EBCs mit einem “Container” aussehen?

Die Grundsituation sieht so aus: Komponente Client und Service sind zusammen zu stecken. Client publiziert dafür einen Event, Service einen Event-Handler. Der Event ist der Output-Pin, der mit dem Event-Handler als Input-Pin verbunden werden muss. Zwischen beiden ist eine Leiterbahn zu legen.

Anmerkung: Ich benutze ab jetzt Begriffe aus der Elektrotechnik ohne Anführungszeichen. Platine, Pin, Bauteil, Leiterbahn usw. scheinen mir einfach so gut zu passen, dass ich sie nicht mehr als Analogie hervorheben will, sondern für den Moment mal zu EBC-Fachbegriffen mache.

Die EBC-Bauteilspezifikationen sehen dafür z.B. so aus:

interface IClient

{

    event Action<string> OnOutput;

}


interface
IService

{

    void ProcessString(string text);

}

Konkrete Bauteile, die diesen Spezifikationen folgen, können dann so zusammengesteckt werden:

IService s = new Service();

IClient c = new Client();

c.OnOutput += s.ProcessString;

Das ist ganz einfach. Aber wenn es um mehr als 2-3 Leiterbahnen geht, dann wird es lästig. Wie kann das also automatisiert werden?

Vom Nutzen eines DI Container

Zunächst dachte ich, ein DI Container hat bei EBC nicht mehr soviel Bedeutung. Doch das stimmt nicht. Er seinen vollen Wert für die Build Phase. In der werden die EBC-Bauteile instanziert. Die legt sie sozusagen auf die Werkbank, bevor sie in der Bind Phase auf einer Platine zusammengesteckt werden.

Im obigen Beispiel besteht die Build Phase aus den beiden Instanzierungen. Mit einem DI Container kann die so aussehen:

IUnityContainer uc = new UnityContainer();

uc.RegisterType<IService, Service>();

uc.RegisterType<IClient, Client>();

IService s = uc.Resolve<IService>();

IClient c = uc.Resolve<IClient>();

Obwohl die EBC-Komponenten keine funktionalen Abhängigkeiten untereinander haben, lohnt sich der Einsatz eines DI Containers. Denn zum einen können Bauteile von anderer Funktionalität abhängen, die nichts mit EBC zu tun hat. Zum anderen können Bauteile Platinen sein und insofern doch von Bauteilen abhängen. Davon später mehr.

Wenn Sie sich auf EBC einlassen, vergessen Sie Ihren liebsten DI Container also nicht! Instanzieren Sie die EBC-Bauteile mit ihm.

Bauteile binden

Die Herausforderung für das Tooling rund um EBCs liegt also nicht in der Instanzierung, sondern in der automatischen Verbindung von Pins, d.h. in der Bind Phase. Die besteht im Beispiel nur aus einer Zeile:

c.OnOutput += s.ProcessString;

Wenn das nun mehr Zeilen werden, wie könnte Ihnen ein Tool die Arbeit abnehmen? Ich stelle mir das derzeit so vor:

ComponentBinder.Bind(c, s);

Schön einfach, oder? So soll es ja auch sein. Die Bind()-Methode soll halt irgendwie dafür sorgen, dass die Input- und Output-Pins der übergebenen Bauteile “zu einander finden”. Ganz allgemein sollen Sie also Bind() Instanzen aller zu verdrahtenden Bauteile übergeben. Wieviele das sind, hängt vom Abstraktionsniveau des Codes ab, in dem die Bindung stattfindet.

Sie können sich z.B. entscheiden, nur mit atomaren Bauteilen zu arbeiten. Dann rufen Sie Bind() nur einmal mit all diesen Bauteilen auf. Für die Bauteile

A->B->C->D

sähe das so aus: Bind(a, b, c, d)

Wenn Sie allerdings Bauteile auf Platinen zu größeren Einheiten aggregieren, dann sieht das anders aus:

A->Platine(B->C)->D

würde zu den Aufrufen Bind(a, platine, d) und Bind(b, c) führen.

Aufgerufen wird Bind() immer von einer Platine. Im einfachsten Fall gibt es nur eine für Ihre ganze Anwendung. Sobald die Sache jedoch etwas größer wird, werden Sie Bauteile auf kleineren Platinen zusammenfassen und sogar Platinen wieder auf größeren zusammenstecken. Davon gleich mehr.

Wie kann eine automatische Bindung von Output- mit Input-Pin stattfinden? Die Pins haben folgende Grundform:

Output: event Action<T> _
Input: void _(T _) {…}

Die Unterstriche stehen hier für Angaben, die für die Bindung zunächst nicht relevant sind. Im ersten Anlauf würde ich einfach mal alle Methoden mit 1 Parameter vom Typ T und ohne Rückgabetyp als Event-Handler bei allen Events vom Typ Action<T> registrieren.

image

Jedem Event sind also potenziell mehrere Event-Handler zugeordnet:

image

Und falls es mehrere Events vom selben Typ gibt, dann sind die Event-Handler mit all diesen Events verbunden:

image

Wer solches Leiterbahnenspaghetti vermeiden möchte, der muss einfach nur zusehen, dass sich die Typen der Events konsequent unterscheiden und dass es für jeden Event-Typ nur einen Handler gibt.

Soweit eine erste einfache Bindungsalgorithmusversion. Eine zweite Version könnte dann auch noch die Namen der Pins mit einbeziehen. Dann würden nur Pins verbunden, deren Typen übereinstimmen und (!) bei denen auch irgendwie die Namen passen. Dazu braucht es eine Konvention. Die könnte so aussehen:

Events haben den Präfix “On”, Event-Handler der Präfix “Process” oder “Handle”. Pins werden zusammengesteckt, wenn sie im Typ übereinstimmen und in dem, was auf den jeweiligen Präfix folgt:

image

Dem Nachrichtentypnamen ist sozusagen noch ein Pinname zugeordnet. Im vorstehenden Bild sind das T.X und T.Y.

Das scheint mir zunächst auszureichen. Das Matching der Output- mit den Input-Pins kann dann so ablaufen:

  1. Sammle alle Output-Pins
  2. Sammle alle Input-Pins
  3. Für jeden Output-Pin…
    1. Finde alle Input-Pins, die zu seinem “qualifizierten Typ” (Typ + Pinname) passen
    2. Wenn es solche Pins gibt, dann registriere sie als Event-Handler…
    3. …ansonsten suche alle Pins, die nur zu seinem Typ passen
      1. Wenn es solche Pins gibt, dann registriere sie als Event-Handler…
      2. …ansonsten hängt der Output-Pin in der Luft. Was tun? Ich denke, das ist eine Fehlermeldung wert – es sei denn, auf die wird ausdrücklich verzichtet.

Schritte 1. und 2. laufen auf allen an Bind() übergebenen Bauteilen ab. Alle Bauteile sind gleich. Wie schon in einem früheren Posting geschrieben, gibt es bei EBC formal keine Client-Service-Abhängigkeiten. Dadurch ist es auch möglich, eine Komponente mit sich selbst zu verbinden. Rekursionen sind also auch möglich.

Platinen

Platinen sind ebenfalls EBC-Komponenten. Sie erfüllen allerdings keine Funktion im Sinne einer Geschäftslogik. Ihre Verantwortlichkeit ist allein die Aggregation von Bauteilen. Platinen bestehen daher vor allem aus einem Konstruktor, in dem die Platinenbauteile zusammengesteckt werden.

Hier ein simples Szenario:

image

Das Bauteil in der Mitte empfängt T-Nachrichten und erzeugt S-Nachrichten. Ob es atomar ist und die Transformationvon T nach S selbst vornimmt oder “nur” eine Platine ist, die andere Bauteile zu diesem Zweck aggregiert, das ist für die anderen Bauteile nicht erkennbar und auch nicht wichtig. Womöglich verändert sich das auch über die Zeit. Die Spezifikation bleibt gleich:

interface IMittelteil

{

    void ProcessX(string _);

    event Action<int> OnY;

}

Im Falle einer Platine sieht die Implementation allerdings speziell aus. Nehmen wir mal an, dass auf dem Mittelteil als Platine zwei andere Bauteile stecken:

image

Dann sähe die Implementation des Mittelteils mindestens so aus:

class Platine : IMittelteil

{

    public Platine(IB b, IC c)

    {

       

        ComponentBinder.Bind(b, c);

    }

   

Die Platine sorgt dafür, dass ihre Bauteile verdrahtet werden. Welche Bauteile das sind, bekommt sie über DI mitgeteilt. Hier tut also der DI Container wieder gute Dienste.

Auch wenn die Aufgabe der Platine denkbar simpel ist (und sie daher viele Abhängigkeiten haben darf), so ist sie wie oben noch nicht vollständig. Ihre Bauteile sind zwar untereinander verdrahtet – aber es fehlt die Verbindung zu den Pins der Platine. Der Input in die Platine muss ja in ihr Bauteil B fließen und Output aus C muss an die Umwelt der Platine weitergereicht werden.

Der Input-Event-Handler der Platine muss dazu mit dem Input-Event-Handler von B verbunden werden. Und der Output-Event von C muss den Output-Event der Platine feuern:

class Platine : IMittelteil

{

    private IB b;


    public
Platine(IB b, IC c)

    {

        this.b = b;

        this.c.OnY += x => this.OnY(x);

       

    }


    public
void ProcessX(string _)

    {

        this.b.ProcessX(_);

    }


    public
event Action<int> OnY;

}

Inputs einer Platine müssen an Inputs von Bauteilen weitergeleitet werden, unverdrahtete Output-Pins von Bauteilen müssen verbunden werdne mit Output-Pins der Platine.

Das ist – wie gesagt – nicht schwierig und sehr regelmäßig - dennoch ein wenig nervig. Im Augenblick sehe ich allerdings noch keinen Weg, um das zu automatisieren. Die Verbindung von Bauteil-Outputs mit Platinen-Outputs wäre möglich, weil dafür nur ein Delegat ad hoc erzeugt werden muss (s. Lambda Funktion im Ctor). Aber was tun mit der Weiterleitung vom Platinen-Event-Handler zum Bauteil-Event-Handler? Vielleicht ist da etwas zu machen, wenn Platinen abstrakte Basisklassen sind oder ihre Event-Handler virtuelle Methoden? Dann könnte man von ihnen zur Laufzeit ableiten und die Event-Handler überschreiben. Hm… darüber muss ich mal nachdenken. Für den Anfang kann ich allerdings auch ohne eine Automatisierung dieses Aspektes der Verdrahtung von EBCs leben.

Injektionen

Wenn eine automatische Bindung grundsätzlich funktioniert, dann wäre der nächste Schritt, in diesen Prozess eingreifen zu können. Ich könnte mir vorstellen, dass ein ComponentBinder Events feuert, wenn er dabei ist, Pins zu verbinden. Auf diesen Events könnten Sie lauschen und eingreifen. Eine Verdrahtung könnte unterdrückt werden. Oder sie könnten einen anderen Event-Handler-Delegaten zurückreichen (z.B. einen für einen Tracer).

Alternativ könnten dem ComponentBinder Prädikate mit anhängenden Kommandos mitgegeben werden. Bei jeder bevorstehenden Bindung könnten die Prädikate geprüft und bei Wahrheit ihre Kommandos ausgeführt werden.

In jedem Fall scheint mir die Injektion von Zwischenstücken zwischen Output- und Input-Pins, wenn sie denn von Hand vorgenommen wird, keine so große Sache. In einer ersten Version eines Binders würde ich sie dennoch raus lassen.

Automatische Dokumentation

Ein interessanter Aspekt ist mir noch zur automatischen Bindung eingefallen: Der Binder “sieht” ja alle Bauteile. Warum sollte er dann nicht auch Auskunft geben darüber, welche das sind und wie er sie verdrahtet hat? Konkret: Warum sollte der Binder nicht eine Dokumentation generieren können über die Schachtelung und Verbindung von Bauteilen? In einem XML-Format ausgegeben könnte daraus anschließend eine Visualisierung der de facto Architektur generiert werden. Wie wäre das?

DI Container hätten das auch immer schon tun können. Haben sie aber nicht. Schade. So kann es denn ein ComponentBinder von vornherein besser machen.

PS: Jetzt hab ich es doch schon getan. Eine erste Version eines Binders für EBCs ist online bei CodePlex unter http://ebcbinder.codeplex.com:

image

Probieren Sie den Binder mal aus. Dann diskutieren wir im Forum des Projektes, wie es damit weitergehen kann.

Dienstag, 23. Februar 2010

Verbindungsstücke – Event-Based Components abhören

Auf zum vorläufigen Endspurt mit den Event-Based Components (EBC). Wie die grundsätzlich definiert werden, habe ich hier beschrieben. Dann ging es darum, wie sie grundsätzlich zusammengesteckt werden. Und schließlich galt es, die Kommunikation noch etwas zu vereinfachen. Um mit EBC zu arbeiten, ist jetzt alles auf dem Tisch.

Aber es kommt noch besser! Denn bisher haben Sie nur gesehen, wie EBC das leisten, was “traditionelle” Komponenten auch leisten. Die EBC tun das natürlich architekturell sauberer, finde ich, aber funktional haben sie keinen Vorsprung. Das möchte ich nun ändern. Dazu müssen wir uns nochmal ansehen, wie EBC “zusammengesteckt” werden.

Ein ganz einfaches Szenario soll da genügen. Komponente Quelle verschickt eine Nachricht Output und Komponente Senke empfängt sie.

image

Hier die Kontrakt-Interfaces und die Nachricht:

interface IQuelle

{

    event Action<Output> OnOutput;

}


interface
ISenke

{

    void ProcessOutput(Output msg);

}


class
Output

{…}

Die Bindung im Rahmen einer “Software-Platine” geschieht dann so:

// Build

IQuelle q = new Quelle();

ISenke s = new Senke();

// Bind

q.OnOutput += s.ProcessOutput;

Anschließend kann die Quelle angestoßen werden und kommuniziert mit der Senke:

((Quelle) q).Run("hello");

class Quelle : IQuelle

{

    public void Run(string text)

    {

        this.OnOutput(new Output {Text = "<"+text+">"});

    }


    public
event Action<Output> OnOutput;

}

Alles easy. Nichts neues, wenn Sie die bisherigen Blogartikel zu EBC verfolgt haben. Jetzt halten Sie sich aber fest…

Nachrichten abfangen

Haben Sie schon mal versucht, die Kommunikation zwischen “traditionellen” Komponenten zu verfolgen? Vielleicht ist eine komponentenorientierte Anwendung mal nicht so gelaufen, wie Sie wollten, und Sie haben gedacht, wenn Sie wüssten, mit welchen Argumenten die Komponenten sich gegenseitig aufrufen, dann könnten Sie leicht heraus finden, wo es hakt, ohne zum Debugger zu greifen.

Ich jedenfalls möchte so ein Tracing in Anwendungen einschalten können. Natürlich ohne dafür eine Änderung in meinem Quellcode vornehmen zu müssen. Das funktioniert auch mit einem DI Container wie Structure Map und automatisch generierten Proxies. Stefan Lieser hat das mal für die Anwendungen gemacht, die wir innerhalb der Clean Code Developer Seminare mit den Teilnehmern entwickeln. Dafür war allerdings schon einiges Spezialwissen nötig.

Mit EBC geht das hingegen ganz einfach. Wenn Sie auf einer Verbindung zwischen zwei EBC lauschen wollen, um z.B. den Nachrichtenfluss zu protokollieren, dann tun Sie das ganz einfach so:

// Bind

q.OnOutput += o => Console.WriteLine("tracing {0}", o);

q.OnOutput += s.ProcessOutput;

Registrieren Sie einen zweiten Event-Handler am Output-“Pin” einer EBC. Der empfängt alle Nachrichten wie die eigentliche Zielkomponente für die Nachrichten. Schließlich sind Events multi-cast Delegaten.

image

Wenn Sie den Lauscher vor der Zielkomponente registrieren, bekommt er zuerst die Nachrichten. Registrieren sie ihn hinterher, dann bekommt er die Nachrichten erst, wenn die Zielkomponente schon fertig ist. Das können Sie auch dynamisch zur Laufzeit tun. Stellen Sie das Abfangen für jeden Output-“Pin” separat ein und aus, wenn Sie mögen.

Nix Reflection, keine dynamischen Proxies, kein Hexenwerk… alles ganz einfach mit dem Abfangen von Nachrichten bei EBC (Interception). EBC schenkt Ihnen sozusagen Aspektorientierte Programmierung (AOP) für manche Szenarien.

Verbindungsstücke

Denken Sie das noch ein Stück weiter: Die Verbindung zwischen Quelle und Senke ist explizit für jede Nachricht. Output-“Pin” wird mit Input-“Pin” “verdrahtet”… Wer sagt da eigentlich, dass diese “Drähte” direkt zwischen den Komponenten verlaufen müssen?

Sie könnten ein Tracing in Form einer generischen Funktionseinheit als Zwischenstück in die Verbindung zwischen Quelle und Senke einsetzen:

image

Das ist bei der Bindung ja leicht möglich:

q.OnOutput += Tracer.Create<Output>(s.ProcessOutput);

Der Tracer ist dann nichts als eine Indirektion zwischen Output-“Pin” und Input-“Pin”:

class Tracer

{

    public static Action<T> Create<T>(Action<T> processor)

    {

        return t =>

            {

                Console.WriteLine("tracing: {0}", t);

                processor(t);

            };

    }

}

Wo hier Console.WriteLine() steht, können Sie natürlich beliebig viel Aufwand für ein generisches Tracing treiben – oder Sie delegieren die Aufgabe an log4net oder SmartInspect.

Oder überlegen Sie ein anderes Szenario für ein Zwischenstück. Wie wäre es, wenn Sie Nachrichten zur Laufzeit aufhalten wollen, bis eine Senke sie (wieder) aufnehmen kann? Sie könnten ein Ventil zwischen Quelle und Senke einsetzen.

image

Das ist bei der “Verdrahtung” ganz einfach:

var valve = new Valve<Output>();

q.OnOutput += valve.Register(s.ProcessOutput);

Weder Quelle noch Senke merken etwas davon, dass der Fluss zwischen ihnen über das Ventil gesteuert werden kann:

((Quelle)q).Run("1");

valve.Close();

((Quelle)q).Run("2");

((Quelle)q).Run("3");

valve.Open();

Die Nachrichten “2” und “3” werden erst bei Aufruf von Open() an die Senke weitergeleitet. Und die Implementation eines solchen Ventils ist trivial:

class Valve<T>

{

    private bool isOpen = true;

    private Queue<T> buffer = new Queue<T>();

   
    public void ProcessMessages(T msg)

    {

        if (this.isOpen)

            this.OnMessage(msg);

        else

            this.buffer.Enqueue(msg);

    }


    public
event Action<T> OnMessage;


    public
Action<T> Register(Action<T> processor)

    {

        this.OnMessage += processor;

        return this.ProcessMessages;

    }


    public
void Open()

    {

        foreach (T msg in this.buffer)

            this.OnMessage(msg);

        this.isOpen = true;

        this.buffer.Clear();

    }


    public
void Close()

    {

        this.isOpen = false;

    }

}

Jetzt denken Sie mal weiter… Was ließe sich mit so einem “Ventil” oder einem vergleichbaren Steuerelement zwischen Komponenten noch alles machen? Sie könnten Ziel-Komponenten dynamisch laden und austauschen. Sie könnten Ziel-Komponenten ihren Zufluss selbst steuern lassen. Sie könnten Load-Balancing betreiben. Sie könnten Nachrichten per TCP verschicken zu einer entfernten Komponente… Das alles und noch mehr würde Nachrichten 1:1 weiterleiten. Früher oder später ;-)

Jetzt denken Sie noch weiter… Was könnten Sie mit den Nachrichten in “Zwischenstücken” alles machen? Sie könnten Sie transformieren, filtern, zerlegen, aggregieren… Und wieder alles, ohne dass die Komponenten selbst davon etwas merken würden.

Wer schon mal davon geträumt hat, wiederverwendbare Komponenten zu entwickeln, der hat jetzt endlich etwas in der Hand. Nicht Geschäftslogik-Komponenten sollten auf Wiederverwendbarkeit getrimmt werden, sondern Infrastrukturkomponenten wie ein solches Ventil. Dafür gibt es aber erst einen Markt, wenn Sie Ereignisorientierung und Nachrichtenorientierung denken.

Solch flexibler Umgang mit nachrichtenindividuellen Komponentenverbindungen ist für mich dann auch der Grund, Nachrichten einen eigenen Typ zu geben. Dann sehen nämlich alle Verbindungen zwischen Komponenten gleich aus. Sie sind vom Typ Action<T>. Und dann kann man generische Infrastruktur bauen, die mit diesen T-Nachrichten etwas tut.

Wer da an Enterprise Integration Patterns denkt, der liegt nicht falsch. Mir gehts aber erstmal nur um eine Flexibilisierung der synchronen Kommunikation zwischen Komponenten.

Jetzt sind Sie dran. Wohin trägt Sie Ihre Phantasie nun, da sie Event-Based Components kennengelernt haben?