Follow my new blog

Samstag, 26. Januar 2013

Softwareentwurf als Video – Ein Experiment

Wie entwirft man Software? Wie kann man über Softwarestrukturen nachdenken und reden, ohne am Code zu kleben?

Dazu entwickeln Stefan Lieser und ich als die Clean Code Advisors seit Jahren einen Ansatz unter wechselndem Namen: alles begann mit Softwarezellen, dann kamen Feature Streams dazu, die haben Event-Based Components auf den Kopf gestellt und im Augenblick steht im Kern Flow-Design. Alles fließt – nicht nur in der Software, sondern auch bei unserer Methode. Wir voran gekommen, haben einen pragmatischen Weg gefunden – aber wir sind sicher noch nicht am Ende [1].

Wie dieser Weg beschritten werden kann, thematisieren wir in Zeitschriftenartikeln und Blogpostings. Und wir führen in Trainings darin ganz konkret ein, zum Beispiel in der Clean Code Developer Akademie oder auch inhouse. Immer wieder werden wir aber auch gefragt, ob es nicht etwas “zum Zuschauen” gäbe, bei dem man verfolgen kann, wie die Anwendung funktioniert.
Bisher musste wir verneinen. Außer einem Vortragsmitschnitt hier und da, in dem ich mal Modellierung präsentiert habe, gibt es keine “Bewegtbilder” zum Softwareentwurf, wie wir ihn verstehen.

Das könnte sich jetzt aber ändern. Stefan und ich haben nämlich ein Experiment gemacht. In Regensburg haben wir uns um ein SMART Board versammelt und mitgeschnitten, wie wir eine Anwendung entwerfen und beginnen zu implementieren.

Das Ergebnis sehen Sie hier:



Wie gesagt, es ist ein Experiment. Alles steht letztlich zur Disposition. Wir freuen uns über Feedback.

Das Video gliedert sich in mehrere Abschnitte:
  • 00:18:18: Erklärung der Aufgabe
  • 02:13:14: Überblick über den Lösungsansatz verschaffen
  • 05:30:11: UI skizzieren
  • 09:24:03: Interaktionen identifizieren
  • Modellierung von…
    • 12:12:17: Inkrement #1
    • 15:58:21: Inkrement #2
    • 24:02:14: Inkrement #3
    • 28:24:05: Inkrement #4
  • 39:25:03: Kontrakte definieren, um zu zweit komponentenorientiert entwickeln zu können
  • 50:27:13: Review der Kontraktcodierung
  • 51:42:09: Review der Implementation des ersten Inkrements
  • Den Code können Sie im Github Repo einsehen. Dort liegen auch die Entwürfe als PDF.
Ob das zu viel oder zu wenig ist, werden Ihre Rückmeldungen ergeben. Aber wir haben uns auch schon eine Meinung gebildet ;-)

Doch nun: Vorhang auf!

Schauen Sie sich das Video an. Danach können Sie unsere Retrospektive zur Produktion lesen.

Retrospektive

Der Dreh hat Laune gemacht. Das war schon mal wichtig :-) Ganz grundsätzlich hat alles ziemlich gut geklappt. Abgesehen von kleinen technischen Problemen haben alle Werkzeuge geschnurrt. Auch der Videoschnitt verlief reibungslos – allerdings muss ich mal über eine Firewire Festplatte und/oder einen Rechner mit mehr Kernen nachdenken. Es wehte einige Stunden ein deftiger Lüfterwind in meiner Bude.

Ob wir das SMART Board allerdings weiter einsetzen, wissen wir noch nicht. Wie die Totale im Video zeigt, ist es mit dem Licht nicht so einfach. Die Projektionsfläche ist hell – aber Stefan und ich erscheinen als Schattenspieler davor. Die Raumhelligkeit hat für uns nicht ausgereicht. Das nehmen Kameras anders wahr als menschliche Beobachter im selben Raum. Das nächste Videoexperiment wird sicherlich ein anderes Darstellungsmittel ausprobieren.

Auch beim Ton müssen wir nochmal schauen, was wir tun können. Einerseits könnten wir etwas lauter sprechen. Andererseits darf die Akustik etwas besser werden. Das Nuscheln müssen wir uns aber in jedem Fall abgewöhnen ;-)

Mit mehreren Kameras aufzunehmen und zwischen ihnen zu schneiden, war in jedem Fall eine Leichtigkeit. Das behalten wir bei. Dadurch ergibt sich eine natürliche Lebendigkeit im Video ohne großen Aufwand. Wechsel zwischen Küche, Fahrrad, Auto, Schreibtisch usw. ohne inhaltlichen Bezug, wie bei anderen Videoproduktionen, können wir uns sparen ;-)

Nicht zufrieden sind wir jedoch mit der Präsentation des Inhalts. Da sind wir einen Kompromiss eingegangen, weil wir organisatorischen Aufwand befürchtet haben, der das Ergebnis suboptimal ausfallen lässt.

Der Kompromiss besteht darin, dass wir die Inkremente im Sinne eines agilen Vorgehens zuerst alle modellieren, um sie erst anschließend eines nach dem anderen zu implementieren. Das entspricht nicht unserem wahren Vorgehen, das ist nicht, was wir empfehlen. Doch wir dachten, so sei es einfacher aufzunehmen, um nicht immer wieder zwischen Modellierung und Implementierung umbauen zu müssen.

Wie sich dann aber herausgestellt hat, war es gar nicht nötig, für die Implementierung bzw. den Review von Code umzubauen. Wir haben einfach mit den drei Kameraperspektiven weitergemacht und am SMART Board über den Code gesprochen. Kein live coding, kein Screenrecording, wie zunächst gedacht.

Das werden wir beim nächsten Mal auf jeden Fall anders machen: design a little, code a little. So muss der Rhythmus sein. Sonst läuft man mit dem Modell bei allen Vorteilen eines expliziten Entwurfs in die Irre. Bubbles don´t crash behält seine Relevanz. Das zeigt das letzte Inkrement im Video. Die Diskussion darüber ist verhältnismäßig lang – und trotzdem kommt etwas heraus, das nicht voll funktionstüchtig ist. Wir haben schlicht nicht alle Eventualitäten durchdacht. Erst “Anwendertests” nach der Implementierung von Inkrement #4 haben gezeigt, dass es eine Lücke in der Funktionalität gab.

Das Problem ist beim letzten Inkrement aufgetreten, da war es dann nicht so schlimm. Aber es hätte uns auch früher passieren können; dann wären die Modelle weiterer Inkremente auf falschen Modellen aufgebaut worden. Dagegen schützt nur, Modellinkremente sofort zu implementieren.

Eine weitere Frage, die wir uns gestellt haben, ist die nach dem Single Responsibility Principle. Haben wir das mit dem Video eingehalten? Ist der Fokus eng genug? Oder haben wir auch hier schon zuviel auf einmal gewollt? Bewusst haben wir unseren Ansatz schon nicht erklärt, sondern ihn einfach unter uns angewandt. Aber ist es vielleicht zuviel, dann auch noch zu implementieren? Die Kontraktdefinition scheint uns in jedem Fall zu ausführlich geraten.

Ausblick

Soweit mal unser erstes Videoexperiment mit Reflexion. Nun kommen Sie. Was ist Ihr Feedback? Was gefällt Ihnen, was sollen wir anders machen? Haben Sie Interesse an weiteren Videodarstellung zum Thema Softwareentwurf? Wünschen Sie sich inhaltlich etwas? Worauf sollten wir näher eingehen? Was weglassen? Können wir den Entwurf in bestimmten Domänen oder von konkreten Szenarien demonstrieren?

Wir sind gespannt, was Sie uns schreiben.

Fußnoten

[1] Dass wir überhaupt an einem Ansatz arbeiten und nicht schlicht OOA/D und UML benutzen ist kurz gesagt: Wir sehen in der Praxis nicht, dass die existierenden Ansätze erstens überhaupt relevante Verbreitung gefunden hätten. Es regiert das Durchwursteln. Zweitens halten wir diese Ansätze im Sinne von Methode bzw. Standarddarstellung für unzureichend und allemal schwer erlernbar.
Unser Bemühen richtet sich daher auf eine pragmatischere, auch im Projektalltag handhabbare Methode ohne Firlefanz, die einen Großteil der Softwarerealität abdeckt.

Wie diese Methode aussieht, haben wir in unterschiedlichen Publikationen immer wieder in Facetten dargestellt. Eine umfassende Beschreibung fehlt allerdings. Auch dieses Video liefert sie nicht. Es bietet nur einen Ausschnitt. Aber vielleicht kommen wir ja noch dazu, ein Buch zu schreiben… ;-) Einstweilen entsteht die derzeit systematischste Beschreibung gerade in einer Artikelserie für die Zeitschrift web & mobile developer.

Montag, 7. Januar 2013

Objektorientierung an der Quelle

Was viele Entwickler so jeden Tag betreiben, soll ja Objektorientierung sein. Java, C#, C++ und einige Sprachen mehr firmieren als objektorientiert. Sogar F# sucht den Anschluss als hybride Sprache.

Und wenn es dann in diesen Sprachen Klassen gibt, aus denen zur Laufzeit Objekte gemacht werden, dann ist das doch auch richtig – oder?

Natürlich kann jeder unter Objektorientierung im Grunde verstehen, was er will. Oder Objektorientierung ist einfach der kleinste gemeinsame Nenner dessen, was irgendwie so genannt wird. Ja, so kann man die Kunst betreiben.

Aber man kann es auch anders tun und sich fragen, was diese Objektorientierung denn ursprünglich mal sollte, wie sie eigentlich gedacht war. Was da herauskommt, finde ich spannend.

Wer hat´s erfunden? Der Alan Kay. Und der sagt zu seiner Erfindung in einer Emailkonversation zum Beispiel das Folgende:

“I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages (so messaging came at the very beginning -- it took a while to see how to do messaging in a programming language efficiently enough to be useful).”

“So I decided to leave out inheritance as a built-in feature until I understood it better.”

“OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”

An anderer Stelle ist von ihm dann auch noch zu hören gewesen:

“I invented the term Object-Oriented, and I can tell you I did not have C++ in mind.”

Das Internet: “Possibly the only real object-oriented system in working order.”

Nun frage ich mich, was der entscheidende Unterschied ist zwischen dem, was Alan Kay mal visionierte und dem, was wir heute haben? Denn ganz offensichtlich ist die heutige Objektorientierung nicht zu dem herangewachsen, was er gewollt hatte.

Mir scheint, der Hauptunterschied liegt im Begriff Message. Bestätigung finde ich dafür auch hier in der Bemerkung über Smalltalk-80, wo der Begriff Message irreführenderweise weiter verwendet wurde, obwohl es nur noch um synchrone bidirektionale Kommunikation zwischen Objekten ging.

Heutige objektorientierte Sprachen betreiben schlicht kein Messaging zwischen Objekten. Sie haben damit den Begriff aus dem Blick verloren, der am Anfang aller Objektorientierung stand (s.o. das erste Zitat).

Im folgenden Code wird also keine (!) Message vom Aufrufer an Objekt o gesendet:

var z = o.f(x, y);

o.p = “…”;

Das erste ist nur ein Unterprogrammaufruf, ein procedure call. Das zweite ist ein Zugriff auf Daten eines Objektes.

Keine Frage, so etwas ist nützlich. Ich denke nicht, dass Alan Kay das abschaffen wollte. Aber er hätte nicht mit der Objektorientierung begonnen, wenn er nicht darüber hätte hinausgehen wollen.

Messaging ist unidirektional. Und Objekte sind mindestens abgeschlossene, zustandsbehaftete Kapseln, die keine Struktur verraten, wenn nicht sogar autonom, d.h. auf eigenem Thread laufend.

Klassen, Vererbung, Polymorphie (über Basisklasse oder Interface)… das sehe ich daher ganz und gar nicht im Kern von Objektorientierung. Alan Kay hat eher an dynamische Programmierung gedacht, wenn er “extreme late-binding of all things” forderte.

Und was bedeutet das? Wenn wir Alan Kay ernst nehmen, dann sollten wir erstens bei dem, was wir so üblicherweise tun, den OO-Mund nicht so voll nehmen. Zweitens sollten wir endlich beginnen, mal richtig objektorientiert zu denken und zu codieren. Manche Sprachen machen das sicher leichter als andere. Und dann schauen wir mal, wohin wir kommen, wenn wir unsere Software intern mehr nach dem Internet modellieren, das Alan Kay für das einzige funktionstüchtige echt objektorientierte System hält.

In diesem Sinne: Merry encapsulation, and happy messaging!

Donnerstag, 3. Januar 2013

Beispielhafte Nichtbeachtung

Wie könnte eine Anwendung aussehen, die dem Prinzip der gegenseitigen Nichtbeachtung folgt? Ich versuche das mal anhand eines simplen Szenarios im Kontrast zu einer mehrschichtigen Anwendung darzustellen.

Das Beispielszenario

Eine Anwendung soll den Index aller Worte in Dokumenten aufbauen. Die Dokumente sind Textdateien (Endung auf .txt) unterhalb eines Wurzelverzeichnisses. Und Worte sind Zeichenketten ohne Leerzeichen, d.h. sie werden durch Leerzeichen von einander getrennt.

Die Anwendung wird mit dem Wurzelverzeichnis gestartet, z.B.

wordindex f:\docs

und liefert den Index in folgender Form auf dem Standard-Output ab:

image

Zu jedem Wort gelistet in alphabetischer Reihenfolge werden die Dateinamen der Dokumente gelistet, in denen es vorkommt.

Das ist alles natürlich keine große Sache. Aber ich denke, es gibt genug her, um zwei Ansätze zu vergleichen. Der Code für beide liegt in einem github Repository. Wer ihn also komplett sehen möchte, ist herzlich eingeladen.

Ansatz #1: Mehrschichtige Anwendung

Bei der mehrschichtigen Variante will ich mich hier gar nicht lange mit Entwurfsschritten aufhalten. Ist doch durch das Muster “Mehrschichtige Architektur” schon alles vorgegeben, oder?

Da gibt es ein Frontend – das hier zugegebenermaßen dünn ausfällt –, da gibt es Domänenlogik und dann gibt es noch Datenzugriff. Alles simpel, aber vorhanden.

image

Für jede Schicht mache ich eine Komponente auf, d.h. es gibt einen separaten Kontrakt und eine binäre Implementation (Assembly). In Visual Studio drücke ich das durch mehrere Projekte aus:

image

Es gibt also drei Projekte – je eines für jede Schicht – und dazu noch eines für die Kontrakte und eines für den Programmstart und den Zusammenbau des Ganzen zur Laufzeit.

Die Interfaces (funktionale Kontrakte) sind simpel. Woher die kommen, lasse ich mal außen vor. Hier fallen sie vom Himmel :-)

image

Dass ich mich für die Schichten für Interfaces entschieden habe, sollte keine Fragen aufwerfen. Aber bei den Daten… Warum ein Interface für den Index?

Ich erwarte, dass der Index selbst noch wieder eine gewisse Funktionalität enthält. Die will ich aber zum Zeitpunkt der Kontraktlegung nicht festschreiben müssen. Was weiß ich, wie die Domäne die Indizierung implementiert? Sie kann Funktionalität zwischen Indexer und Index verteilen, wie sie mag.

Indem ich also den Index auch nur als Interface in den Kontrakte angebe, sind die Kontrakte schnell geschrieben und die parallele Entwicklung an allen Schichtenkomponenten kann losgehen.

Das mag für manche neu sein – aber am Ende ist das ein alter Hut. Eben Komponentenorientierung.

Abhängigkeiten

Die aus meiner Sicht unseligen Abhängigkeiten bestehen nun zwischen den Implementation der funktionalen Kontrakte. Zur Compilezeit sind sie von Interfaces abhängig, zur Laufzeit von deren Implementationen.

image

Wenn ich Console testen will, muss ich eine IIndexer-Attrappe haben. Und wenn ich den Indexer testen will, muss ich eine Attrappe für IDocuments haben. So ist das halt. Hört sich ganz normal an, oder? Quasi unvermeidbar. Dafür gibt es ja schließlich Mock-Frameworks.

Bei Programmstart werden die Implementationen dann zusammengesteckt. Das passiert über Dependency Injection:

image

Auch normal. Dass ich hier keinen DI-Container benutze, ist vernachlässigbar.

Das funktioniert alles tadellos. Aspekte sind erkannt (Schichten) und für produktive Entwicklung und Wiederverwendung in Komponenten herausgelöst. Über Interfaces wird entkoppelt. Funktionale Abhängigkeiten sind sauber ausgerichtet von oben nach unten. Jede Schicht ruft nur die unmittelbar darunter liegende.

Das funktioniert tadellos. Wer mag, probiert es mit dem Code im Repo aus.

Gedanken zum Entwurf

Oben wollte ich noch nichts zum Thema Entwurf sagen. Und auch jetzt möchte ich nicht den Entwurf nachholen. Aber eines Kommentares kann ich mich nicht enthalten:

Als ich die Anwendung entworfen habe, habe ich mich schwer getan. Nicht sehr schwer natürlich. Ist ja alles total triviales Zeugs. Doch ich habe gemerkt, dass ich einen Moment geschwommen bin.

Die Schichten zu finden, war nicht schwer. Die sind ja durch das Muster vorgegeben und für das Szenario auch klar erkennbar. Aber wie dann die Interfaces genau aussehen sollten… dazu fiel mir erstmal nichts ein.

IIndexer war simpel. Da geht es zentral um die Zweck der Anwendung. Also bin ich gleich auf die obige Definition verfallen. Ich habe einfach Wünsch-dir-was gespielt :-)

Aber wie sollte IConsole aussehen? Vor allem aber: Wie sollte IDocuments aussehen? Was würde ein Indexer von IDocuments wollen?

Bei diesem Entwurfsansatz habe ich mich da sehr gehemmt gefühlt. Ich hätte eigentlich mit TDD loslegen müssen zu implementieren, um dabei schrittweise herauszufinden, was ich von IDocuments brauche.

Das habe ich mir angesichts des einfachen Beispiels dann aber gespart und habe einfach mal getippt ;-) Wie sich herausstellte, lag ich da nicht falsch.

In Summe ist mein Eindruck also, dass mir der Ansatz der Mehrschichtigkeit zwar im gaaanz Groben irgendwie hilft – nur dann wird es schnell schwierig. Ich werde nicht geleitet bei der Verfeinerung (Wie sehen die Schichten denn konkret aus?). Und die Abhängigkeiten machen dann bei der Implementation keinen Spaß: Entweder muss ich bei Tests Attrappen einsetzen oder ich muss von unten nach oben entwickeln. Beides macht keine Laune.

Ansatz #2: Nichtbeachtende Anwendung

Jetzt gilt es. Wie sieht eine Anwendung aus, die nicht mehrschichtig ist? Wie kann das Problem mit den funktionalen Abhängigkeiten vermieden werden? Im Repo finden Sie dazu auch einen Vorschlag, die Solution obliviouswordindex:

image

Von Grundaufbau her sieht sie gar nicht so anders aus. Es gibt wieder drei Projekte für die Aspekte Frontend, Domäne und Persistenz. Warum auch nicht? Es sind Aspekte, die es lohnt, deutlich separat zu implementieren.

Und es gibt wieder ein “Hauptprogramm” und ein Projekt für das, was den Aspekten gemein ist: Daten.

Die Beziehungen zwischen diesen Teilen sehen allerdings anders aus!

Zunächst einmal ist zu bemerken, dass es keine funktionalen Kontrakte gibt. Interfaces für die Aspekte sind unnötig. Es gibt zwischen ihnen ja keine funktionalen Abhängigkeiten. Das Frontend kennt keine Domäne und die Domäne weiß nichts von Persistenz.

Die Implementationen liegen funktional “nichtbeachtend” nebeneinander:

image

Es gibt lediglich gemeinsame Erwartungen an Daten. Und das ist ja auch völlig ok. Daten sind vergleichsweise “ungefährlich”, wenn sie keine großartige Funktionalität enthalten. Dann können sich Änderungen an Logik nicht so leicht fortpflanzen.

Daten stehen für die unvermeidliche logische Kopplung. Aber nur weil es die gibt, ja, geben muss, heißt das ja nicht, dass darüber hinaus auch noch funktional gekoppelt werden muss.

Das Hauptprogramm ist wieder von allen Implementationen abhängig, weil es sie irgendwie zusammenstecken muss. Aber das ist auch sein einziger Job: die Integration.

Die Komponenten implementieren hingegen Operationen, d.h. Logik.

Das ist die Basis der Architektur des Prinzips der gegenseitigen Nichtbeachtung: die Trennung der Aspekte Integration und Operation. Diese Aspekte gibt es immer. Sie sind domänenunabhängig. Auch das ist anders als bei der Mehrschichtigkeit. Die trennt ebenfalls Aspekte – allerdings sind das inhaltliche.

Vielleicht ist es gar nicht so schlecht, das mal wie folgt gegenüber zu stellen:

image

Soweit die Grundarchitektur. Da gibt es auch immer noch Abhängigkeiten bei der “Nichtbeachtung”. Sowas, sowas ;-)

Dennoch ist die rein praktische Erfahrung, dass es simpler ist, die Operationsschicht mit den nicht funktional abhängigen Operationen zu implementieren. Es sind schlicht keine Attrappen nötig.

Entwurf

Wie komme ich nun zu diesen Operationen? Das geht über einen schrittweisen Entwurf. Ich muss dafür keinen Test schreiben, sondern einfach das Problem verstehen und einen Lösungsansatz formulieren können.

Es ist im Grunde wie die Erarbeitung einer Sprache. Es geht um Sätze und Worte. Die Operationen entsprechen den Worten, sie sind das Grundvokabular für Sätze. Größeres kann ich dann aus diesem Vokabular und schon gebildeten Sätzen zusammenbauen.

Beim Entwurf für das Beispielszenario könnte das so aussehen: Am Anfang stelle ich mich ganz dumm. Ich weiß nur, dass ich Input von der Kommandozeile bekomme und dass der Index auf dem Standard-Output rauskommen soll. Dazwischen passiert “es” dann irgendwie:

image

Bei der Ausgabe bin ich mir sicher, dass ich die nicht weiter verfeinern will. Eine IIndex-Datenstruktur letztlich in einen Text zu serialisieren, ist eine Sache von wenigen Zeilen.

Und wie ist es mit der Dokumentenverarbeitung? Da spielt jetzt die Musik. Die einfach so ohne weitere Verfeinerung zu implementieren, wäre mir zuviel. Kann man machen – muss man aber nicht ;-)

Also zoome ich mal rein und denke mir die Verarbeitung wie folgt:

image

Zuerst werden Dokumente geladen – eines nach dem anderen. Und mit jedem wird der Index erweitert. Aus Load docs fließen also einzelne Dokumente an Build index.

Jetzt wieder die Frage: so lassen oder weiter verfeinern? Jede dieser Funktionseinheiten wäre schon wieder einfacher zu implementieren. Vor allem, weil sie ja auch für sich, also nichtbeachtend realisiert würden. Ich könnte eine Methode Load_docs() entwickeln, die nichts mit einer Methode Build_index() zu tun hätte – und umgekehrt. Das war bei der Mehrschichtigkeit anders.

Aber ich finde die Funktionalität immer noch zu grob. Also verfeinere ich weiter. Dokumente laden, erfolgt in drei Schritten:

image

Zuerst einmal muss ich den Wurzelpfad aus der Kommandozeile herausfischen, dann suche ich alle überhaupt relevanten Dokumente, also die .txt-Dateien, und schließlich lade ich für jeden Dateinamen den Dateiinhalt.

Bitte beachten Sie, wie aus 1 Input-Wert, dem Wurzelpfad, n Output-Werte, die Dateinamen werden.

Anschließend wird zu jedem Dateinamen aber wieder nur 1 Dokument produziert.

Document wie IIndex sind im “Datenkontrakt” definiert, da sie relevant zwischen Aspekten sind. Andere Datentypen gibt es nicht auf globaler Ebene; innerhalb von Aspekten benutze ich gern dynamische Typen.

Mit den Funktionseinheiten auf dieser Zoomebene bin ich jetzt zufrieden. Eine weitere Zerlegung ist nicht nötig; ich bin bei den Operationen angekommen.

Aber der Indexbau muss noch näher betrachtet werden. Auch dort sehe ich eine Schrittfolge vom Dokument bis zum Index:

image

Zuerst wird jedes Dokument in seine Worte zerlegt, die ich Funde (occurrence) nenne. Das sind Tupel bestehend aus Wort und Dateiname. Wie angedeutet, definiere ich dafür aber keinen eigenen Typ. Das wäre Overkill. Alle Operationen gehören ja zum Aspekt Domäne. Da kann man sich untereinander so gut kennen, dass man sich auf die Struktur dynamischer Typen verlassen kann.

Der Filterschritt unterdrückt Worte mit weniger als 3 Zeichen. Und bei der Registrierung wird der Fund dem Index hinzugefügt.

Wenn es keine Funde mehr gibt, fließt am Ende der Index hinaus.

Angezeigt wird das Ende eines Stromes durch eine fließende Null. find docs schließt damit den Strom der Dateinamen ab, load doc fährt damit fort nach dem letzten Dokument usw. Das ist ein probates Mittel für die stromorientierte Verbindung von Operationen. Letztlich macht es Rx auch so – allerdings über einen eigenen Event, der das Ende eines Stromes anzeigt. Ein Endesignal muss aber sein. Insofern unterscheiden sich Ströme von Listen (zu denen auch IEnumerable<T> gehört).

Ich hoffe, mit dieser schrittweisen Verfeinerung habe ich niemandes Phantasie strapaziert. Dass so ein Indexaufbau funktioniert, sollte auf der Hand liegen.

Wie weit ich verfeinere, hängt von meinem Vorstellungsvermögen ab. Diese drei Zerlegungsebenen finde ich einfach naheliegend. Aber Sie können das Problem natürlich auch anders angehen. Entscheidend ist nicht, wie Sie konkret zerlegen, sondern dass sie es überhaupt tun. Damit unterscheiden Sie nämlich Integration und Operationen.

Die Operationen get root path, output index, parse doc usw. stellen jetzt das Grundvokabular dar. Das sind alles black boxes. Wie die implementiert werden, ist egal für die Integration. Alle sind aber unabhängig von einander. Sie definieren sehr kompakte Leistungen, wenn man ihnen passenden Input liefert.

Mit diesem Grundvokabular habe ich “Sätze” gebildet, z.B. Load docs. Die liegen auf einem höheren Abstraktionsniveau. Die Zerlegungsebenen sind insofern keine Schichten, sondern Strata im Sinne von Sussmans & Abelsons “Structure and Interpretation of Computer Programs”.

Das kann man von IConsole oder IIndexer bei der mehrschichtigen Architektur nicht sagen. Die Domain ist nicht soetwas wie die Persistence auf höherer Abstraktionsebene, sondern etwas anderes. Eine solche Zerlegung hilft deshalb nur wenig bei der Problemlösung.

Eine schrittweise Verfeinerung wie hier vorgenommen jedoch, die hilft. Es geht auf jeder Ebene nämlich um das Ganze – nur eben mit immer feingranularerem Vokabular.

Implementation

So viele Bildchen, wann kommt denn endlich Code? Bubbles don´t crash, heißt es doch. Bisher wurde nur geredet und nichts substanzielles geschafft – oder?

Weit gefehlt. Alles, was Sie bisher an Bubbles verbunden mit Pfeilen gesehen haben, ist schon Code. Hier der Beweis:

image

Das sind die “Blubberdiagramme” übersetzt in eine texttuelle Notation (DSL). Die hätte ich auch gleich hinschreiben können – aber Bilder sind übersichtlicher, lassen sich schneller auf einem Blatt notieren im kreativen Prozess und machen es einfacher, einen Entwurf anderen zu erklären.

Aber keine Angst, die hübschen Bildchen sind nicht weg, nur weil ich sie in Text übersetzt habe. Sie lassen sich jederzeit aus dem Text generieren:

image

Das funktioniert sogar nicht nur in der Entwicklungsumgebung, sondern auch auf dem Binärcode. Falls mal also nur eine .EXE vorliegt, kann der hier eingesetzte Visualizer auch aus ihr diese Integrationen lesen und in Bilder rückübersetzen. Aber das nur am Rande.

Wichtig ist, dass ich rüberbringe, dass die Diagramme nicht nur eine optionale Nettigkeit sind, sondern Code darstellen. Sie entwerfen und Implementieren letztlich gleichzeitig. Entwurf ist damit keine verschwendete Zeit. Sie tun etwas für Ihr Problemverständnis, Sie tun damit etwas für eine saubere Lösungsstruktur – und nun haben Sie auch schon einen wichtigen Teil der Lösung codiert: die Integration.

Die nun noch nötige Implementation der Operationen ist aber auch nicht schwierig. Dafür benutze ich jetzt auch eine 3GL, z.B. C#. Damit können Operationen in Methoden übersetzt werden oder ganze Klassen. Wie es mir von Fall zu Fall am besten passt.

Beispiel TextfileDocuments: Die Operationen find docs und load doc lassen sich schön in statische Methoden übersetzen. Warum auch nicht? Ein Interface ist ja nicht mehr nötig. Zustand gibt es auch nicht. Also spricht nichts gegen statische Methoden:

image

Da load doc bei jedem Aufruf, also für jeden Dateinamen, ein Dokument erzeugt, ist eine Funktion angebracht. Anders bei find docs. Für 1 Wurzelpfad werden n Dateinamen erzeugt. Da ich die nicht als Liste mit einem Mal zur Weiterverarbeitung leiten möchte, sondern die Dokument für Dokument einzeln stattfinden soll, implementiere ich die Operation als Prozedur mit Continuation für die Dateinamen.

Dass Operationen leichter zu testen sind, zeigt bei diesem Aspekt jedoch noch nicht so sehr. Auch in der mehrschichtigen Anwendung war diese Funktionalität unabhängig.

Anders ist das bei der Domäne. Die ist in der mehrschichtigen Architektur abhängig von der Persistenz. Jetzt aber nicht mehr:

image

Hier habe ich mich für Instanz-Operationen entschieden, weil die Domäne einen Zustand hat: den Index. Der wird ja langsam aufgebaut (s. Register_occurrence()).

Keine der Operationen kennt nun aber eine Persistenzoperation. Es gibt keine Request/Response-Kommunikation, sondern nur message passing. Klingt für mich übrigens total objektorientiert ;-)

Beachten Sie, wie ich hier dynamische Typen einsetze. Die Operationen vertrauen darauf, dass ihr Input eine passende Struktur hat. Von wo der kommt und wohin ihr Output geht… das wissen sie jedoch nicht. Deshalb sind die einfach zu testen.

Die Abwesenheit von Request/Response lässt Methoden auch tendenziell kleiner sein. Denn wenn Sie merken, dass Sie in einer Operation etwas brauchen, dann müssen Sie sie im Grunde beenden, einen Output erzeugen – und eine andere Operation aufmachen, in die das, was Sie brauchen, als Input einfließt. Aus dem üblichen

void f() {
  // vorher
  string r = g(42);
  // nachher
}

würde

void f_vorher(Action<int> continueWith) {
  // vorher
  continueWith(42);
}

void f_nachher(string r) {
  // nachher
}

Wenn Sie das so sehen, wirkt es natürlich irgendwie umständlich und künstlich. Aber mit ein bisschen Übung, verliert sich diese mentale Reibung. Dann ist das eine ganz natürliche Denkweise; dann können Sie nicht mehr anders. Oder besser: dann wollen Sie nicht mehr anders – müssen jedoch manchmal.

Aber solches Vorgehen ist natürlich kein Zwang. Sie können beim Design entscheiden, bis zu welcher Granularitätsebene Sie so denken wollen – und wann Sie damit aufhören. Wenn Sie durch Entwurf auf Ihre Operationen gekommen sind, dann können Sie sich in deren Implementation auch austoben. Da ist quasi wieder alles erlaubt.

Dennoch sollten Sie die Integration-Operation-Trennung (und den Verzicht auf Request/Response) immer im Hinterkopf behalten. Sie sind es, die zu den kleineren Methoden führen. Eine Methode integriert entweder oder sie operiert. Und wenn sie integriert, dann sind darin keine Kontrollstrukturen und keine Ausdrücke erlaubt. Wie lang kann dann eine Methode noch werden? ;-)

Deshalb ist es auch nicht schwer, eine textuelle oder grafische DSL für die Integration wie oben gezeigt zu realisieren. In der Integration passiert einfach nichts. Keine Logik. Nur “Zusammenstecken”.

Komposition

Da gibt es nun eine Integration notiert in einer obskuren DLS. Und es fliegen eine Menge Operationen im Raum herum. Wie kommt beides zusammen zu einer lauffähigen Anwendung, wie sieht der Kompositionsschritt bei diesem Ansatz aus.

Der ist ganz einfach. Dependency Injection gibt es ja nicht. Allerdings müssen die Integrationsbeschreibung und die Operationen bei einer Runtime registriert werden. (Das ist aber nur eine Möglichkeit, wenn Sie sich eben für die DSL entscheiden. Sie können diese Art der Strukturierung auch anders umsetzen.)

image

Die Konfiguration der Runtime sucht sich die Operationen aus den angegebenen Assemblies selbst heraus; sie sind ja mit einem Attribut versehen.

Und dann geht es auch schon los. Der Einfachheit halber und weil nicht mehr gebraucht wird, schalte ich die Runtime zur Verarbeitung der Datenfluss-Integration auf synchrone Verarbeitung. Mit den Kommandozeilenargumenten geschickt an den Port .run beginnt die erste Operation ihre Arbeit.

Zusammenschau

Den Unterschied in den zwei Ansätzen sehen Sie hoffentlich:

Erstens: Bei der Mehrschichtigkeit verteilt sich Logik (Kontrollstrukturen und Ausdrücke) über alle Ebenen. Das ist in diesem Beispiel noch gar nicht so deutlich geworden, weil die Schichten so dünn sind. Aber in größeren Szenarien gibt es ja auch innerhalb Schichten Abhängigkeitshierarchien.

Solch verschmierte Logik macht es schwer, schnell und einfach und präzise den Ort zu finden, wo man für eine Verhaltensänderung eingreifen muss.

Und wenn man ihn gefunden hat, dann ist der Test nicht so leicht, weil es in Abhängigkeitshierarchien eigentlich keine Unit Tests gibt, sondern immer nur Integrationstests. Denn wenn Funktionseinheit A von B abhängt und ich für den Test von A eine Attrappe_B brauche, dann teste ich nicht nur, ob in A die richtigen Entscheidungen getroffen werden, sondern auch noch, ob B erwartungsgemäß aufgerufen wird. Ein korrekter Umgang mit B oder gar auch noch mit C und D stellt jedoch eine Integrationsleistung dar: Logik in A wird mit Logik in B und C und D zu etwas größerem Integriert.

Zweitens: Zumindest für mich ist es schwer, Abhängigkeitshierarchien zu entwerfen. Wenn sie einmal da sind, dann kann ich sie malen. Aber wie komme ich dahin? Und vor allem, wie komme ich in agiler Weise dahin? Wenn Agilität bedeutet, in Inkrementen zu denken, wie kann ich solche Hierarchien in Inkrementen wachsen lassen? Und bleiben dann diese Inkremente sichtbar?

Das alles finde ich schwierig, schwierig. Nicht, dass ich nicht selbst lange Jahre so versucht hätte zu entwickeln. Das habe ich wahrlich. Letztlich ist das jedoch im Vergleich zu dem, wie einfach es mir heute fällt, alles ein Krampf gewesen. Den sehe ich auch regelmäßig, wenn Teams versuchen, solche Beispiele im Rahmen von Assessments zu lösen. Das ist dann für mich ein ganz praktischer Beweis dafür, dass in den Köpfen eine Menge Konzepte herumschwirren, wie das mit dem Entwurf gehen könnte und sollte… Nur kommt dann kein Gummi auf die Straße. Aus “Mehrschichtige Architektur, MVC und Entwurfsmuster plus SOLID” resultiert kein systematisches Vorgehen, das erstens zu verständlichen und evolvierbaren Strukturen führt, die zweitens dann auch noch im Team effizient implementiert werden können. Leider, leider ist das so.

Deshalb erlaube ich mir, ernsthafte Zweifel anzumelden, dass das mit der Mehrschichtigkeit großen Wert hat. Nicht zuletzt deshalb, weil wir das ja nun auch schon mehr als ein Jahrzehnt probieren und die Softwaresysteme immer noch nach kurzer Zeit unwartbar werden.

Klar, es gibt Fortschritte. IoC und DI-Container und Mock-Frameworks haben etwas gebracht. Heute wird schon mehr entkoppelt als früher. Aber “mehr” bedeutet noch nicht gleich “genug”. Ich sehe da noch deutlichen room for improvement.

Zwei Beispiele dafür sind die strikte Trennung von Integration und Operation, also ein Integration Operation Separation Principle (IOSP) ;-), sowie das Principle of Mutual Oblivion (PoMO). (Ein bisschen Englisch zwischendurch darf schon mal sein ;-)

Dass Schritte über das Gewohnte hinaus dann erstmal mühsam sind, ist halt so. So ist es immer. Auch von prozeduraler zu objektorientierter Denke war der Übergang mühsam. Oder von Objektorientierung zur Funktionalen Programmierung. Bei Veränderungen knirscht es – was ja aber nicht bedeutet, dass es hinterher nicht besser sein kann. Wenn ich heute 20 Liegestützt kann und mal 25 oder 30 mache, dann tut das auch weh. Aber wenn ich es morgen und übermorgen usw. wieder tue, dann geht der Schmerz vorbei; ich werde kräftiger. Und in einem Monat kann ich 50 Liegestütz. Eine vorher ungeahnte Menge ist möglich, weil ich mich durch Müh und Plage durchgebissen habe.

So ist es mit vielen anderen Fähigkeiten auch, körperlichen wie geistigen. Ob man diesen Weg geht, ist natürlich eine Frage der persönlichen Ökonomie. Lohnt es sich, den Veränderungsschmerz auszuhalten? Das muss letztlich jeder für sich entscheiden. Wer schon 20 Liegestützt kann, mag abwinken. Kraft für mehr braucht er nicht. Und aus dem Alter, wo er Mädels beeindrucken wollte, ist er auch raus ;-) Aber wer sich noch schwer tut beim Entwurf von Software, wer noch vor einem unwartbaren Codeverhau sitzt… kann der sich leisten, Veränderungsschmerz zu vermeiden, der auf dem Weg zu einer anderen Denke entstehen würde?

Naja, ist vielleicht eine Frage für ein längeres Gespräch bei einem Kaltgetränk.

Abhängigkeiten im close-up

An dieser Stelle abschließend noch ein Wort zum Thema Abhängigkeiten, das für mich schon im vorhergehenden Artikel so wichtig war. Dabei sehe ich ja einen großen Unterschied, der Ihnen vielleicht nicht so groß erscheint. Deshalb nochmal durch die Lupe darauf geschaut:

Was ist der Unterschied zwischen

string[] Split(string text) {…}

und

void Split(string text, Action<string[]> parts) {…}

Keiner. Beide Methoden zerlegen einen Text in irgendwelche Teile, z.B. Worte oder Zeilen. Beide Methoden liefern diese Teile in Form eines Array ab.

Dass die erste Methode ihr Ergebnis über den Rückgabewert meldet und die zweite über einen Delegaten, ist im Grunde nur syntactic sugar für den Aufrufer.


var parts = Split(“…”);

vs


string[] parts = null;
Split(“…”, _ => parts = _);

Der marginale Unterschied ist jedoch nur oberflächlich. Darunter lauert ein viel größerer. Der besteht nämlich in der Flexibilität der prozeduralen Variante mit dem Delegaten. Dort kann man sich nämlich entscheiden, ob jedes Teil einzeln oder alle Teile zusammen abgeliefert werden. Man kann wählen, ob man eine Liste oder einen Stream produziert. Die Prozedur kann sogar entscheiden, gar kein Ergebnis zu liefern; dann ruft sie den Delegaten einfach nicht auf.

void Split(string text, Action<string[]> parts) {…}
void Split_to_stream(string text, Action<string> part) {…}

Bei der Funktion hingegen gibt es immer ein Ergebnis und nur eines.

Aus meiner Sicht ist es damit schwerer, den Schritt von synchroner zu asynchroner Verarbeitung zu machen. Der Delegatenaufruf stellt ein Ereignis dar und kann leicht asynchron oder gar für jeden Teil parallel verarbeitet werden:

Split_to_stream(“…”, part => new Task(()=>Process(part)).Start());

Das ist für mich der erste Nachteil unserer Fixierung auf Request/Response-Kommunikation. Die hat uns das Leben schon vor 15 Jahren schwer gemacht, als wir versucht haben, objektorientierte Denke einfach so auf verteilte Systeme auszudehnen. Und nun sorgt sie wieder für Reibung. Diesmal beim Übergang zu Mehrkernprozessoren.

Den Ort eines Requests zwangsweise gleichzusetzen mit dem Ort für die Bearbeitung eines Response, ist schlicht eine enge Kopplung. Aber nicht nur für den, der den Request bearbeitet.

Das Problem setzt sich fort beim Aufrufer. Wo ist der Unterschied zwischen

void f() {
  …
  var parts = Split(text);
  …
}

und

void f(Func<string,string[]> Split) {
  …
  var parts = Split(text);
  …
}

Wieder gibt es keinen grundlegenden Unterschied. Die Methode f() benutzt eine Dienstleistung zur Zerteilung von Zeichenketten. Dass die im zweiten Fall als Delegat hineingereicht wird, sozusagen parameter injection, ändert daran nichts. Das ist nur eine Variante von dependency injection, die mal ohne Interface auskommt und nicht auf Klassenebene arbeitet.

In meinem Sinne ist dadurch noch nichts erreicht in puncto Abhängigkeitsreduktion. f() ist in beiden Fällen abhängig:

image

Um f() allein zu testen, muss eine Attrappe her. Und dann testet man eben nicht nur die Funktionsweise der Logik in f(), sondern auch noch, ob Split() korrekt benutzt wird. Das ist aber ein anderer Aspekt, nämlich der der Integration: spielt ureigene f()-Logik korrekt zusammen mit Split()-Logik?

Das klingt subtil, geradezu wie Haarspalterei. Aber je länger ich mich damit beschäftige, desto größer scheinen mir die negativen Auswirkungen der Vernachlässigung dieser Feinheit.

Der Trick ist einfach, dass f() von einer Semantik abhängig ist, nicht nur von einer Form. Die Ursache dafür ist in diesem Fall der Request/Response-Aufruf.

Jetzt dieses kleine Szenario etwas anders:

void f_produce_text(Action<string> Text_produced) {
  …
  Text_produced(text);
}

void f_process_text_parts(string[] parts) {
  …
}

Sehen Sie den Unterschied jetzt? f_produce_text() ist unabhängig von einer Semantik des Delegaten. Es wird immer noch eine Methode injiziert, aber die tut nichts für den Aufrufer. Sie nimmt nur etwas entgegen und transportiert es weiter. Das macht die “Nichtbeachtung” aus.

Beide Methoden können nun ohne Attrappen getestet werden. Beide Methoden tun jetzt auch nur eines: sie implementieren Logik, d.h. sie stellen Operationen dar.

Natürlich muss dann irgendwo alles zusammenkommen. Das geschieht bei der Integration:

void f(Func<string, Action<string[]>> Split) {
  f_produce_text(t =>
    Split(t ,
      f_process_text_parts));
}

Die Integration ist nun abhängig von konkreter Semantik. Sie stellt selbst Semantik her und braucht dafür Sub-Semantiken. Aber sie enthält keine Logik. Deshalb ist der Test einer Integration einfach. Der kann sich nämlich darauf konzentrieren festzustellen, ob die Subsemantiken korrekt “verdrahtet” sind. Das ist alles. Im Falle von f() ist dafür nur ein Test nötig.

Dass eine Integration abhängig ist von weiteren Integrationen bzw. Operationen, ist unvermeidbar. Aber das ist nicht mehr so schlimm, weil in ihr eben keine Logik steckt. Deshalb ist es ja auch möglich, Integrationen wie oben gezeigt in einer trivialen DSL zu notieren.

Die f()-Integration würde damit notiert so aussehen:

f
f_produce_text, split
split, f_process_text_parts

Also: Ein Test von Integrationen ist einfach, auch wenn dafür Attrappen nötig sind/sein sollten, weil nur wenige Tests gebraucht werden. Meist reicht einer. Und der kann auch noch auf recht hoher Abstraktionsebene angesiedelt sein.

Und ein Test von Operationen ist einfach, weil die Operationen keine funktionalen Abhängigkeiten haben, d.h. sie kennen keine Semantik von Konsumenten ihrer Daten. Attrappen sind nicht für ihre Tests nötig.

Fazit

Ich kann nicht mehr anders denken als im Sinne von IOSP und PoMO. Alles andere kommt mir zäh und umständlich und im Ergebnis wenig flexibel vor. Aber das ist einfach meine Erfahrung nach 3 Jahren konstanter Übung.

Ganz praktisch drückt sich das übrigens darin aus, dass ich keine Mock-Frameworks mehr benutze. Wo ich früher noch mit Rhino Mocks o.ä. rumgehüsert habe, da kann ich mich jetzt entspannen. Falls mal eine Attrappe nötig ist, schreibe ich die in ein paar Zeilen selbst. So bin auch ich weniger abhängig – von Tools.

Dass ich Sie nun überzeugt habe, das Lager der Mehrschichtigkeit bzw. der “Abhängigkeitsdenke” zu verlassen, glaube ich nicht. Aber ich habe Ihnen hoffentlich einen Impuls gegeben, Ihre OO- und Architekturwelt ein bisschen anders zu sehen. Das Übliche ist nicht alles. Es geht auch anders. Und nicht nur die Funktionale Programmierung hat etwas zu bieten.

Vielleicht, ganz vielleicht versuchen Sie ja aber einmal – wenn die Kollegen Sie nicht sehen –, die beiden Prinzipien IOSP und PoMO an einem Beispiel wie dem obigen selbst anzuwenden. Machen Sie Ihr persönliches Coding Dojo mit einer Application Kata. Das Wortindex-Szenario könnten Sie wiederholen oder erweitern (Stoppwortliste, Zeilennummern der Worte je Dokument, Dokumentenindizierung als Service in der Cloud). Oder Sie nehmen sich ein anderes Szenario vor. Bei den Clean Code Advisors finden Sie weitere Application Katas.

Falls Sie Fragen haben, melden Sie sich einfach per Email – oder diskutieren Sie mit in der Google Group oder XING-Gruppe zum Thema Flow-Design/Event-Based Components.

Montag, 31. Dezember 2012

Prinzip der gegenseitigen Nichtbeachtung

Mehrschichtigkeit ist immer noch tief in den Köpfen verankert. Und wenn nicht in den Köpfen, dann im Code. Übel daran ist nicht nur, dass Mehrschichtigkeit viele Fragen weder beantwortet noch stellt, sondern dass damit Abhängigkeiten geschaffen werden. Code der einen Schicht hängt irgendwie von Code der anderen ab. Die eine Schicht braucht die andere.

Aber welche Schicht sollte welche brauchen? Darüber hat Jens Schauder in einem Blogartikel nachgedacht. Den habe ich gelesen – und dann habe ich gestutzt. Jens erkennt zurecht Abhängigkeiten als Problem – doch dann sieht er eine Lösung nur darin, dass er sie umdreht. Aus dem üblichen

image

wird bei ihm nur

image

Die Domäne soll Interfaces definieren, die UI/Persistence dann nutzen bzw. implementieren [1].

Das empfinde ich als keinen großen Schritt nach vorne. Klar, irgendwie wird jetzt die Domäne etwas mehr betont. Das ist nicht schlecht. Aber das grundsätzliche Problem der Abhängigkeiten wird nicht angegangen.

Was ist dieses grundsätzliche Problem der Abhängigkeiten? Naja, dass das Abhängige das Unabhängige braucht, um etwas leisten zu können. Wer vom Alkohol abhängig ist, der ist nicht arbeitsfähig ohne. Und wo ein UI abhängig ist von der Domain, da kann das UI nichts tun, solange es keine Domain-Schicht in der Hand hat.

Auch das hochgelobte Prinzip IoC macht da keinen großen Unterschied. Aus statischen Abhängigkeiten werden damit nur dynamische. Zur Laufzeit – also auch bei Tests – muss die Abhängigkeit erfüllt werden. Attrappen sind insofern weniger Lösung als Problem, dito DI-Container. Beide kaschieren die fundamental starre Verbindung, die Abhängigkeit selbst.

Ich sehe dafür auch keine Entsprechung in der sonstigen Technik. Wenn ich UI+Domain+Persistence mal zusammen als Application bezeichne, dann könnte die Analogie doch ein Motor sein, oder?

image

Eine Application besteht aus Teilen, ein Motor auch. Nur im Zusammenspiel aller Teile ergibt sich die gewünschte Funktion des Ganzen.

Jetzt aber der Unterschied, der entscheidende:

In der Application braucht und kennt UI die Domain, und bei Jens braucht und kennt Persistence ebenfalls Domain. UI wie Persistence sind unvollständig ohne Domain. Oder allgemeiner: das Abhängige ist unvollständig ohne das Unabhängige.

Im Motor hingegen… da weiß eine Zündkerze nichts vom Zylinder. Ein Zylinder weiß nichts von einer Zündkerze. Die Kurbelwelle weiß nichts von Kolben und umgekehrt.

In der materiellen Technik gibt es keine Abhängigkeiten wie in der Software.

In der materiellen Technik gibt es nur Teile, die für sich genommen eine Form und eine Funktionsweise haben. Sie folgen dem, was ich mal

Das Prinzip der gegenseitigen Nichtbeachtung
(principle of mutual oblivion (PoMO))

nennen will.

Eine Zündkerze beachtet kein anderes Teil. Ein Kolben, eine Kurbelwelle, ein Keilriemen auch nicht. Alle sind für sich geformt und ausgeprägt.

Dass man sie zusammenfassen kann zu etwas Größerem: klar. Dieses Größere definiert einen Kontext und fordert, dass die Teile zusammenpassen. Es ist die Existenzberechtigung für die Teile. Ohne das Größere müssten wir gar nicht drüber reden.

Aber das große Ganze kann eben in unterschiedlicher Weise seine Teile formen. Es kann das wie beim Motor tun. Das bedeutet, die Teile beachten einander nicht – sondern dienen dem Ganzen, das sie integriert.

Oder das Ganze kann es tun wie in der Software üblich. Das bedeutet, es macht die Teile abhängig von einander.

Konkret steckt diese Abhängigkeit in zwei unheiligen Verbindungen:

  • Es wird Input mit Output verbunden: Request/Response ist der vorherrschende Kommunikationsstil zwischen den Softwareteilen.
  • Es werden mehrere Funktionalitäten im selben Interfaces zusammengefasst.

Die erste unheilige Verbindung macht den Übergang von synchroner zu asynchroner und damit zu verteilter Kommunikation schwierig. Außerdem führt sie zu einem unbeschränktem Wachstum von Methoden.

Die zweite unheilige Verbindung macht die Umkonfiguration von Funktionalität schwierig – trotz allem ISP- und IoC-Aufwand, den man treiben mag.

Beim PoMO hingegen ist das alles kein Thema mehr. Denn beim PoMO gibt es keine Abhängigkeiten mehr zwischen “Kooperationspartnern”. Funktionseinheiten kennen einander nicht mehr. “Dienstleister” UI kennt keinen “Dienstleiste” Domain oder umgekehrt, dito “Dienstleister” Persistence.

Wenn die Anwendungsarchitektur dem PoMO folgt, dann sieht sie zunächst so aus:

image

Welch Wonne! Keine Abhängigkeiten, kein Testattrappendrama!

Aber natürlich sind die “Schichten” jetzt gänzlich unverbunden. Da entsteht noch keine Gesamtfunktionalität im Sinne der Application.

Dafür tut nun die Application etwas. Und das tut sie ganz offiziell. In der Abhängigkeitsarchitektur war das nicht so offensichtlich. Aber bei PoMO geht es nun nicht mehr anders. Insofern führt PoMO zu einer fundamentalen Separation of Concerns:

UI, Domain und Persistence gehören zum Concern (Aspekt) “Operation”. Und die Application vertritt den Aspekt “Integration”. Die Application verbindet die Teile so, dass sie zusammen etwas sinniges tun. Diese Verbindung könnte so aussehen:

image

Grün integriert, schwarz operiert. Teile wissen nichts davon, ob und wie sie verbunden sind. Und die integrierende Funktionalität weiß nichts davon, wie die Teile funktionieren. Sie ist ausschließlich von deren Form abhängig.

Und genau das macht den Unterschied aus zwischen einer PoMO-Architektur und dem Üblichen: Bei den üblichen Abhängigkeiten ist der Abhängige nicht nur von der Form des Unabhängigen abhängig, also dass zum Beispiel B eine Funktion mit einer Signatur wie Func<string,int> implementiert. Nein, A ist auch vom Namen der Funktion abhängig und damit von ihrer Semantik, dem, was sie leistet.

Das klingt ganz natürlich. Aber es ist eine Last. Das ist eine doppelte Abhängigkeit: eine von Form und Inhalt. Und dann obendrein noch die Abhängigkeit davon, dass diese Funktion auf einem bestimmten Interface implementiert ist und auch noch, dass sie ein Ergebnis zurückliefert. Puh… das sind ganz schön viele Abhängigkeiten, oder?

Bei PoMO jedoch, da kennt ein A kein B. Und es gibt keine Rückgabewerte; Daten fließen unidirektional. Und ob Methoden von der einen oder einer anderen Funktionseinheit angeboten werden, ist der integrierenden Funktionalität egal. Sie bringt “Angebote” einer Funktionseinheit mit “Nachfragen” anderer zusammen. Das ist alles.

Integration interessiert sich somit nur für Semantik. Das muss sie ja auch, denn sie definiert ein höheres Abstraktionsniveau.

PoMO löst damit mehrere Probleme von Abhängigkeiten. Manche dadurch, dass Abhängigkeiten abgeschafft werden. Manche dadurch, dass Abhängigkeiten fokussiert werden im Sinne von SoC und SRP.

Das scheint mir the way to go, um mal raus aus dem Mehrschichtigkeitssumpf zu kommen.

Fußnoten

[1] Am Anfang seines Artikels stellt Jens die Frage, was denn Teams überhaupt mit einem Pfeil “–>” in einem Diagramm meinen. Ob da immer Klarheit herrsche? Das kenne ich auch: Es wird bildlich entworfen, Kästchen, Kreise, Pfeile, Linien werden gemalt… Aber was bedeuten die? Zwei Entwickler, drei Meinungen ;-)

Für mich gibt es zwei zentrale Symbole für Beziehungen zwischen Funktionseinheiten:

  • Pfeil, A –> B
  • Lolli, A –* B

Der Lolli bezeichnet eine Abhängigkeitsbeziehung: A ist abhängig von B, d.h. A braucht B in irgendeiner Weise. A hat zur Laufzeit ein B in der Hand.

In den meisten UML-Diagrammen und auch bei Jens steht für diese Art der Beziehung allerdings der Pfeil. Der ist mir dafür aber verschwendet, weil bei ihm gar nicht recht klar wird, was die Pfeilspitze aussagt. Ein Pfeil ist anders als eine Linie asymmetrisch. Das ist schon passend zur Abhängigkeit. Abhängiges und Unabhängiges haben unterschiedliche Rollen. Doch die Pfeilspitze trägt für mich dabei zuviel Gewicht. Sie ist verschwendet bei einer simplen Asymmetrie.

Bei einem Datenfluss hingegen kommt die Pfeilspitze viel besser zum Einsatz. Da zeigt sie deutlich die Flussrichtung. Das entspricht ihrer üblichen Bedeutung von “Bewegungsrichtung”. In Diagramme stehen Pfeile für mich also dort, wo Daten fließen, z.B. von Funktionseinheit A zu Funktionseinheit B.

Solcher Datenfluss ist auch asymmetrisch; Datenproduzent und Datenkonsument haben unterschiedliche Rollen. Und Datenfluss ist unidirektional.

Freitag, 28. Dezember 2012

Der Feind des Besseren ist der heutige Erfolg

Eigentlich soll ja alles immer besser werden, oder? Die Prozessoren der nächsten Jahre sollen auf die eine oder andere Weise besser sein als die heutigen – so wie die heutigen besser sind als die der 1990er und die wiederum besser als die der 1980er usw. Auch unsere Autos sollen immer besser werden – so wie sie seit mehr 125 Jahren immer besser geworden sind. Das selbe gilt für Fernseher, Mixer und Solarzellen.

Aber nicht nur solch handfeste Dinge sollen besser werden. Auch bei Dienstleistungen mögen wir es, wenn die sich zum Besseren verändern. Wer möchte heute noch einen Gesundheitscheck wie vor 20 oder gar 100 Jahren? Wo wären wir, wenn die Flugabfertigung immer noch wie vor 60 Jahren abliefe? Selbst beim Friseur schätzen wir den Fortschritt.

Dito bei der Arbeitsweise unserer Unternehmen, oder? Und genauso bei der Software. Auch die soll besser werden, vom Betriebssystem bis zum Smartphone Game. Selbstverständlich nehmen wir die Software, die wir selbst entwickeln, da nicht aus.

Ja, das ist eine Welt, wie sie sein soll: alles wird immer besser und besser.

Was ist “besser”?

“Besser” ist dabei ein relativer Begriff in zweierlei Hinsicht. Zum einen steckt in “besser” ein Vergleich zum Bisherigen. Zum anderen gilt “besser” genauso wie das vorgelagerte “gut” nur in einem Kontext, d.h. in Bezug auf eine Umwelt – und darin in Bezug auf einen Wertmaßstab. Es geht mithin um einen Unterschied, der einen Unterschied macht.

Ein Fernseher mit Fernbedienung ist nicht einfach besser als einer ohne Fernbedienung. Aber die Fernbedienung ist schon mal ein Unterschied zwischen beiden.

Doch wenn dieser Unterschied #1 dazu führt, dass Käufer eine andere Entscheidung treffen (Unterschied #2), d.h. die Fernbedienung sie motiviert, den Fernseher mit Fernbedienung zu kaufen statt den ohne Fernbedienung… dann beurteilen wir den Fernseher mit Fernbedienung als besser im Vergleich zu dem ohne.

Der Wertmaßstab ist in diesem Fall zum Beispiel der Umsatz, der mit Fernsehern gemacht wird. Der Fernseher mit Fernbedienung ist besser in Bezug auf den Umsatz – aber vielleicht schlechter in Bezug auf den Kalorienverbrauch der Fernsehzuschauer (Wertmaßstab Gesundheit) oder den Anfall von Elektronikschrott (Wertmaßstab Nachhaltigkeit).

Wenn wir nun in die Welt schauen, dann – so glaube ich – stimmen wir überein, dass vieles über die Zeit tatsächlich besser und besser geworden ist. Der Spruch “Früher war alles besser.” stimmt also nicht pauschal, auch wenn uns der heutige Stand mancher Entwicklung frustriert [1].

Bedingungen für die Möglichkeit zur Verbesserung

Verbesserungen in allen Bereichen unseres Lebens scheinen so natürlich – aber wie kommt es eigentlich dazu? Wie funktioniert diese Evolution unserer Hilfsmittel und Dienstleistungen?

Mir scheint es zwei zentrale Bedingungen dafür zu geben:

  1. Es müssen überhaupt Ideen für Veränderungen produziert werden. Was könnte der Unterschied #1 zum Ist-Zustand sein? Dafür braucht es ein Milieu, das motiviert, solche Ideen zu produzieren. Sensibilität für Unzufriedenheit, Kreativität, Mut sind nötig.
  2. Die Ideen für Veränderungen müssen implementiert werden können, um zu sehen, ob es zu einem positiven Unterschied #2 in Bezug auf einen für Produzenten wie Konsumenten relevanten Wertmaßstab kommt.

Die Natur als Beispiel

Die Natur macht uns das sehr schön seit Milliarden von Jahren vor. Sie erfüllt beide Bedingungen: die erste durch genetische wie epigenetische Veränderungen, die zweite durch Reproduktion.

Krankheit und Tod zeigen auf, wo Lebewesen noch nicht zufriedenstellend an die Umwelt angepasst sind. Mutationen sind äußerst kreative Veränderungen – oft jenseits unseres Vorstellungsvermögens; wir hätten es nicht besser machen können. Und Mut hat die Natur genügend, da sie durch keine Glaubenssätze beschränkt ist.

So werden Lebewesen von Generation zu Generation besser und besser bzw. erhalten sich die Güte ihrer Überlebensfähigkeit.

Der Markt als Beispiel

Auch der Markt macht uns vor, wie die Bedingungen erfüllt werden können. Konkurrierende Anbieter generieren kontinuierlich Ideen für Veränderungen, die sie über neue Modelle an den Markt bringen.

Vor allem im Bereich des Materiellen funktioniert der Markt damit wie die Natur. Besser wird es durch Generationen von Modellen. Ein Fernseher hat eine Lebensdauer, ebenso ein Flugzeug und ein Röntgengerät. Spätestens nach dieser Lebensdauer kann es durch etwas Besseres ersetzt werden.

Tod als Herz der Evolution

Natur und Markt gleichen sich. In ihnen schlägt das selbe Herz der Evolution: der Tod. Das in der Vergangenheit Gute macht durch seinen Tod ganz automatisch Platz für das inzwischen besser gewordene.

Das Ganze - die Summe aller Lebewesen, d.h. das Ökosystem, bzw. die Summe aller Modelle, d.h. der Markt – erhält seine eigene übergeordnete Lebensfähigkeit also dadurch, dass seine Teile in einem ständigen Prozess von Werden und Sterben stehen.

Allemal gilt das für seine kleinsten Teile, aber durchaus für Subsysteme größerer Granularität. Die Lebensdauer bzw. die Dauer des Verbleibs im System wächst dabei tendenziell mit der Größe eines Subsystems.

Perverse Unsterblichkeit

Lebewesen sterben. Das gehört zu ihrer Definition. Misslich ist es für uns Menschen, dass wir von unserer Sterblichkeit wissen. Sie macht uns nur allzu oft Angst. Da helfen alle Maßnahmen der Gesundheitsvorsorge und Reparaturmedizin auch nicht. Nach rund 100 Jahren machen wir Platz für die Nachgeborenen.

Materielle Produkte haben auch nur eine überschaubare “Lebenserwartung”. Selbst bei bester Pflege fordern Physik (z.B. durch Reibung oder thermische Kräfte) oder Chemie (z.B. durch Oxidation) früher oder später ihren Tribut. Und wo das dem Hersteller zu lange dauert, da setzt er auf planned obsolescence.

Aber wie ist es bei Unternehmen? Wie ist es bei Software?

Bei immateriellen Hilfsmitteln [2] herrscht ein anderes Ideal: sie sollen unbegrenzt lange existieren. Niemand gründet ein Unternehmen mit einer Idee von dessen Lebenserwartung [3]. Und niemand schreibt Software mit einem Verfallsdatum [4].

Ist das nicht pervers, d.h. anormal, verkehrt, unnatürlich – für etwas, das ständig besser werden soll?

Markt und Natur, also die umfassenden Systeme, sollen nicht besser werden, sondern beherbergen das, was immer besser werden soll, um sich als Ganzes zu erhalten [5]. Die umfassenden Systeme kennen keinen auf gleicher Ebene existierenden Konsumenten. Es sind autopoietische Systeme, sie erhalten sich selbst. Wenn sie dienen, dann eher nach innen, ihren Konstituenten gegenüber.

Für mich sieht es so aus, als würden wir Unternehmen sowie Software heute in einem Widerspruch herstellen bzw. betreiben:

Wir strukturieren sie einerseits wie materielle Produktmodelle, d.h. vergleichsweise starr. Das haben wir ja auch über Jahrhunderte gelernt. Sie erfüllen zu einem Zeitpunkt einen Zweck, sind temporär also gut. Eine Verbesserung können wir erst mit einem neuen Modell erwarten.

Andererseits denken wir sie unsterblich, d.h. als übermaterielles System, das es durch ständige Veränderung im Inneren zu erhalten gilt.

Ja, was denn nun? Innen starres Hilfsmittel, das über Generationen evolviert – oder innen flexibles System mit unbegrenzter Lebenserwartung?

Da passt doch irgendetwas nicht, oder?

Hinderlicher Erfolg des Immateriellen

Die Quelle für diesen Widerspruch sehe ich in einer scheinbaren Wahl, die wir haben. Immaterielles wie ein Unternehmen oder Software unterliegt keinem natürlichen Verfall – also können wir die Lebenserwartung selbst bestimmen. Und da bestimmen wir mal, dass die unendlich sein soll. Geht doch. Und ist bestimmt unterm Strich billiger, als immer wieder etwas Neues von Grund auf herzustellen, oder?

Dass sowohl Unternehmen wie Software dann doch nicht ewig existieren, ist eine andere Sache. Shit happens. Potenziell könnten sie es jedoch. Darum gilt es, das anzustreben.

Der Erfolg zu einem bestimmten Zeitpunkt scheint in die Zukunft verlängerbar. Ein Auto funktioniert ja auch mit guter Wartung nicht nur heute, sondern auch noch in Jahrzehnten.

In Wirklichkeit steckt hier jedoch das Missverständnis. Erfolgreich heute – also in Bezug auf heutige Umwelt und Wertmaßstäbe gute Systeme – bedeutet nicht, erfolgreich morgen.

Umwelt und Wertmaßstäbe für Unternehmen und Software sind solchem Wandel unterworfen, dass in ihnen ständig umgebaut werden muss. Ein treffendes Bild scheint mir das einer stehenden Welle: die äußere Form bleibt trotz allem Fluss im Inneren erhalten.

Solange Erfolg mit einem Ist-Zustand an Strukturelementen und Beziehungen gleichgesetzt wird, funktioniert das aber nicht. Erfolg muss davon abgekoppelt werden. Welche Konstituenten ein System ausmachen, ist für die langfristige Existenz des Systems als Ganzem unwichtig. Es gibt nichts per se Erhaltenswertes. Es gibt immer nur einen Systemaufbau, der angesichts einer Umwelt und eines Wertmaßstabs gut genug ist – und sich leicht an Veränderungen anpassen lässt.

Anpassungsfähigkeit ist die einzige erhaltenswerte Eigenschaft. Alles andere steht ständig zur Disposition. Es gibt kein einfach so extrapolierbares Erfolgsrezept im Konkreten, sondern um im Abstrakten.

Praktische Konsequenz

Vielleicht sind unsterbliche Unternehmen und Softwareprodukte wünschenswert. Derzeit sehe ich jedoch nur Versuche zu solcher Unsterblichkeit unter zunehmenden Schmerzen. Wir wissen einfach nicht so richtig, wie wir Unsterbliches aufbauen müssen. Oder wenn wir es wissen, dann setzen wir dieses Wissen nicht systematisch um.

Was also tun? Wir müssen raus aus dem Widerspruch.

Die Blaue Pille nehmen würde bedeuten, bei den aktuellen Konstruktionsweisen zu bleiben. Dann sollten wir aber zumindest die Lebenserwartung von Unternehmen und Software von vornherein begrenzen. “Wir gründen unseren online Shop mit einer Lebenserwartung von 8-12 Jahren.”, “Wir legen die Software auf eine Nutzungsdauer von 3-5 Jahren aus.” – und danach ist Schluss. Ende. Tot. Unternehmen und Software werden dann ersetzt.

Da diese Endlichkeit allen von Anfang an klar ist, kann vorgesorgt werden. Ableger können rechtzeitig gezeugt werden, die neue Erkenntnisse umsetzen – wozu das schon länger Existierende nicht wirklich fähig ist. Die Ausgründung, die Neuentwicklung sind dann keine Überraschungen, sondern ganz natürlich.

Erfolg heute wäre dann kein Ballast mehr für das Bessere. Erfolg wäre nicht narzistisch, sondern bescheiden.

Die Rote Pille nehmen würde hingegen bedeuten, die aktuelle Konstruktionsweise umzukrempeln. Das scheint mir schwieriger – aber nicht unmöglich. Wir müssten lernen, Unternehmen und Software intern eher so zu strukturieren wie ein Ökosystem. Das würde bedeuten, ihre Konstituenten würden evolvieren wie der Automarkt oder die Arten auf einer einsamen Insel.

Hier würde Erfolg per definitionem nicht dem Besserem im Wege stehen. Anpassung wäre in die Systeme eingebaut.

Blaue Pille oder Rote Pille – oder ein Pillencocktail? Ich weiß nicht. Nur sollte es nicht so weitergehen wie bisher. “Aber wir haben doch die letzten 5 Jahren tolle Umsätze gemacht, wir hatten Erfolg… Warum sollten wir jetzt etwas verändern?” darf kein Argument mehr sein, mit dem Veränderungen geblockt werden.

Wir müssen die Bedingungen für die Evolution herstellen: Ideen müssen nicht nur her, sondern auch in bunter Vielfalt implementierbar sein. Evolution ist verschwenderisch. Ressourcenverbrauch auf momentane Bedürfnisse hin zu optimieren, steht längerfristigem Überleben im Wege.

Der wahre Feind der Verbesserung lauert also ständig schon im eigenen System: es ist der heutige Erfolg. Diese Situation müssen wir systematisch bekämpfen.

Fußnoten

[1] Warum wird der Spruch dennoch immer wieder geäußert. Je älter Menschen werden, desto eher scheinen sie ihn auszusprechen. Ich denke, das liegt daran, dass es mit dem Alter erstens überhaupt möglich wird, einen Vergleich zwischen früher und heute anzustellen. Und zweitens hat der Spruch weniger mit den Eigenschaften eines Gegenstandes oder einer Dienstleistung zu tun, als vielmehr mit dem Wertmaßstab.

Es kann sein, dass der vergleichsweise unverändert ist; er hat sich nicht auf die Co-Evolution von Angeboten und Umwelt eingelassen.

Oder es kann sein, dass er sich einfach nur in eine andere Richtung entwickelt hat. Mit zunehmendem Alter verschieben sich Bedürfnisse eben.

[2] Unternehmen subsummiere ich hier mal unter dem Begriff Hilfsmittel. Gegenüber seinen Kunden, ist ein Unternehmen natürlich kein Hilfsmittel, sondern produziert welche. Aber Eigentümer und Angestellte sehen Unternehmen natürlich als Hilfsmittel zur Existenzsicherung.

[3] Ausnahmen gibt es natürlich: Filmproduktionsfirmen haben u.U. eine bewusst auf die Herstellungsphase begrenzte Lebensdauer. Auch Fonds können so organisiert sein, dass ihre Firma nur für die Laufzeit existiert. Oder “Blasenfirmen”, die nur solange irgendwie lebensfähig und vor allem vielversprechend sein müssen, bis sie jemand kauft.

[4] Von Software, die durch absehbare Gesetzesänderungen nur begrenzt relavant ist, sehe ich einmal ab. Meist betreffen solche Termine der Umwelt ja auch nur Teile einer Software.

[5] Erhaltung kann natürlich auch als eine Form der Verbesserung angesehen werden – aber auf einer höheren Ebene. Wenn die Natur heute aber anders aussieht als vor 100 Millionen Jahren, für wen macht das einen Unterschied #2?

Freitag, 21. Dezember 2012

Verteilte Weihnacht mit der Flow Runtime

Für die Freunde des Flow-Designs hier zu Weihnachten eine kleine Überraschung: Flows verteilen mit der Flow Runtime einfach gemacht. Als Beispiel soll Bestellung und "Aufbau eines Weihnachtsbaums dienen.

Wer kennt das nicht: Vor lauter Weihnachtsfeier- und Jahresendprojektstress wird der Kauf eines Weihnachtsbaums bis zum letzten Tag verschoben. Und dann muss man sehen, was übrig bleibt. Das macht die Familie gar nicht glücklich.

Aber jetzt gibt es eine Weihnachtsbaumanwendung für den Windows Desktop :-) Mit der kann man sich einen Weihnachtsbaum in verschiedenen Größen bestellen. So siehts aus:

image

Mit dem Slider auf der rechten Seite stellt man die gewünschte Zahl der Äste ein – und schon wird der Weihnachtsbaum hergestellt und “aufgebaut”. Naja… nur auf dem Bildschirm. Aber immerhin. Bevor es gar keinen gibt… ;-)

Die Herstellung des Weihnachtsbaums übernimmt eine TreeFactory:

image

In ihr gibt es drei “Herstellungsstationen”, die ein Baum nacheinander durchläuft. Der Flow dafür sieht so aus:

image

Zuerst werden nur die Äste in Form von *-Reihen erzeugt, dann werden diese Reihen so eingerückt, dass das charakteristische Dreieck entsteht, und schließlich kommt darunter ein kleiner I-Stamm.

Lokale DIY-Herstellung

In der ersten Ausbaustufe wird der Weihnachtsbaum lokal hergestellt, sozusagen xmas tree fabbing. Die TreeFactory läuft in der Desktop-Anwendung im Rahmen eines allgemeineren “Weihnachtsfabrik”-Flusses. Weihnachtsbäume sind ja auch nur eine, was man zum Fest herstellen könnte:

image

Die Anwendung bestellt beim Start sogleich einen Baum mit 2 Ästen - und danach immer wieder, wenn der Schieberegler bewegt wird. Und das alles from the comfort or your living room :-)

image

Auslagerung der Produktion

Das Weihnachtsbaum fabbing ist hübsch modern. Aber leider kommt dabei nur ein Weihnachtsbaum aus “Plastik” heraus. Ein natürlicher Weihnachtsbaum wäre schöner, oder? Oder einer, bei dem man aus verschiedenen Materialien wählen kann? Jedenfalls scheint eine Auslagerung der Weihnachtsbaumproduktion Vorteile zu bieten.

Aus der lokalen Weihnachtsbaumproduktion soll deshalb eine entfernte werden. Allerdings ohne große “Off-Shoring-Schmerzen”. Wie kann das mit Flows gehen?

Aus der Desktop-Anwendung wird ein Desktop-Client und die Produktion wandert in einen Server.

Dazu wird der Flow xmasfactory in der Anwendung durch eine generische so genannte Stand-In Operation ersetzt:

image

Die sieht nach außen immer genauso aus wie die Funktionseinheit, die sie ersetzt – intern jedoch leitet sie Input über ein Kommunikationsmedium weiter an die entfernte Funktionseinheit und leitet deren Output zurück in den lokalen Flow. Im Beispiel geschieht das mit WCF als API und TCP als Medium.

Der Code ändert sich dadurch wie folgt:

image

Die WcfStandInOperation bekommt eine lokale Adresse, auf der sie entfernten Output empfängt, und eine entfernte Adresse für die “off-shore” xmasfactory, an die sie Input sendet.

In der Flow-Definition wird der bisherigen lokalen xmasfactory der Name der Stand-In Operation mit “#” voran gesetzt. Dadurch wird die Stand-In Operation adressiert, behält über die Instanzkennung [1] nach dem “#” aber die Information, welche Funktionseinheit sie ersetzt.

Die WcfStandInOperation kommt aus der Assembly npantarhei.distribution.wcf, die der Client nun referenzieren muss. Die ist nun Teil des NPantaRhei Repositorys.

image

Soweit der Client. Er wird im Grunde einfacher. Die Servicefunktionalität entfällt und wird ersetzt durch ein Stand-In.

Aber keine Sorge, der Server, in dem sie neu aufgehängt wird, ist leicht herzustellen. Der enthält die extrahierte Funktionseinheit nun allein auf oberster Ebene in einem Flow:

image

Daran ist keine Veränderung vorzunehmen; sie muss sogar so aussehen, wie zuvor, weil sie sonst nicht durch das Stand-In korrekt repräsentiert würde.

Allerdings wird die xmasfactory nun direkt über Flow-Ports angesteuert. Wie sollen die heißen, damit sie automatisch bestimmt werden können? Aus xmasfactory.Build_tree wird .Build_tree@xmasfactory.

Dieser kleine Trick ist nötig, weil in einer Portbeschreibung nur ein Punkt vor dem Portnamen stehen soll. Die Beschreibung .xmasfactory.Build_tree wäre daher ungültig. Also wird der Portname nach vorne gezogen und der Funktionseinheitsname mit “@” dahinter gesetzt. Das ist nicht besonders schön, funktioniert aber erstmal. So können aus der Instanzkennung und dem Portnamen vom Stand-In die serverseitigen Portbeschreibungen bestimmt werden und umgekehrt.

Damit Requests an den Server zu Input für den Flow werden und Output als Response zum Stand-In zurückfließen kann, wird die Flow Runtime in einem so genannten Operation Host gehalten:

image

Der Operation Host empfängt über einen transportmediumspezifischen Transceiver Daten als Input für Flows in der Runtime; und Output aus Wurzelports der Runtime leitet er über den Transceiver dorthin zurück, von wo der zugehörige Input gekommen ist. Den Bezug stellt er über eine out-of-band Correlation-Id her, die er dem Input bei Empfang aufprägt und die über den ganzen Flow erhalten bleibt.

Was sich vielleicht kompliziert anhört, ist in der Codepraxis aber simpel:

image

In der Flow-Definition taucht die xmasfactory wie bekannt aus der lokalen Anwendung 1:1 auf. Ansprechbar wird sie gemacht durch die globalen Port des Wurzel-Flows wie oben beschrieben.

Ansonsten kommt nur der WcfOperationHost dazu, der die Flow Runtime aufnimmt und eine Adresse erhält, auf der er angerufen werden kann.

image

Fertig!

image

Mehr ist nicht zu tun, um eine Funktionseinheit entfernt zu betreiben. Der Flow im Client merkt davon im Grunde nichts. Und die entfernte Funktionseinheit auch nicht. Sie müssen also nicht immer wieder neue Servicedefinitionen vornehmen wie bei WCF. Mit denselben Bausteinen – Stand-In Operation und Operation Host – können Sie jede Funktionseinheit verteilen. Nur die Daten, die über das Transportmedium als Input dorthin und als Output von dort zurück fließen, müssen [Serializable] sein.

Fazit

Die Verteilung von Flows sollte so einfach wie möglich sein. Ich glaube, das ist nun ganz grundsätzlich der Fall. Die WCF-Bausteine sind dabei nur erste einfache Adapter für ein Transportmedium als Beispiele dafür, wie es geht. Nähere Informationen dazu und auch zur Behandlung von Exceptions in verteilten Flows, in der dotnetpro 2/2013.

Ich hoffe, es ist rüber gekommen, dass Sie Flows jetzt lokal entwickeln und testen können – und anschließend einen beliebigen Teil einfach verpflanzen. Dass die Kommunikation dann asynchron wird, ist nicht so schlimm, weil Flows darauf ohnehinn viel besser vorbereitet sind.

Indem Sie vor der Verteilung zum Beispiel ein Operation im zu entfernenden Flow auf asynchrone Verarbeitung setzen oder den Verarbeitungsmodus der Flow Runtime auf asynchrone Breitenorientierung setzen, können Sie deklarativ ganz einfach simulieren, wie sich die Verarbeitung nach der Verteilung verhalten wird.

Oder sie betreiben die Flow Runtime für den Client und die Flow Runtime für den Server im selben Adressraum mit einem kleinen Bus verbunden statt über WCF. Dazu finden Sie eine Hilfestellung in der Assembly npantarhei.distribution, auf die npantarhei.distribution.wcf aufbaut.

Fußnoten

[1] Instanzkennungen sind schon lange ein Feature der Flow Runtime. Sie kommen immer dann zum Einsatz, wenn eine Funktionseinheit in einem Flow an mehreren Stellen mit dem selben Namen benutzt werden soll.

Das ist bei Stand-In Operationen auch der Fall. Die selbe Stand-In Operation soll für verschiedene entfernte Funktionseinheiten stehen können.

Dienstag, 18. Dezember 2012

Die TDD Single Responsibility

Gerade wird wieder eine TDD Demo über Twitter herumgereicht. Corey Haines hat sich an die Kata Roman Numerals gemacht.

image

Mal abgesehen davon, dass TDD anscheinend ein unerschöpfliches Thema ist und die Katas auf die Dauer langweilig werden… Mir gefällt die Darstellung aus einem anderen Grunde nicht so gut.

Corey gibt sich Mühe. Alles läuft seinen kanonischen TDD Weg. Es könnte ein schönes Video sein. Wäre da nicht die ständige Überraschung.

Ja, es hört sich so an, als würde Corey in die Lösung des Problems “Übersetzung arabischer Zahlen in römische” stolpern. Er zaubert Testfälle aus dem Hut und erstaunt sich dann immer wieder selbst mit der Lösung.

Und ich meine hier wirklich die Lösung und nicht den Code.

Da scheint mir ein Grundproblem im Umgang mit TDD zu liegen. Das habe ich auch neulich auf den XPdays in Coding Dojos gesehen. Das Muster ist so:

  1. Es wird ein Problem vorgestellt.
  2. Es wird mit dem Codieren à la TDD begonnen.

Das Ergebnis? Regelmäßig kein Code, der die Aufgabe vollständig erfüllt [1].

Mit dieser Realität sollten wir nicht streiten, denke ich. So ist es einfach. Man bemüht sich redlich um die rechte TDD-Schrittfolge. Das kommt mir vor wie beim Tanzen. Alle starren gebannt auf ihre Füße und hoffen, niemanden anzurempeln. Nur leider geht dabei das große Ganze verloren. Beim Tanzen der Spaß an der Bewegung und am Miteinander. Und bei der Softwareentwicklung lauffähiger Code. Vor lauter TDD-Rhythmus und versuchtem Design funktioniert es nicht mal.

Wie frustrierend, wie tragisch. Kein Wunder, dass auch 2012 immer noch TDD hoch und runter evangelisiert werden muss.

Dabei scheint mir die Rettung der Situation einfach: mehr Systematik.

Ja, tut mir leid, dass ich mit so einem lästigen Wort komme. Das klingt nach Einschränkung, nach viel Aufwand ohne schnellen Nutzen… doch das Gegenteil ist der Fall. Systematik macht frei. Aus Komplexem macht sie Kompliziertes.

Fehlende Systematik überfrachtet TDD. TDD soll plötzlich die ganze Softwareentwicklung retten. Endlich wird das mit der Korrektheit besser. Und dann auch noch bessere Dokumentation durch TDD. Und außerdem höhere Evolvierbarkeit durch besseres Design (lies: bessere Strukturen). Vor allem aber nicht zu vergessen: eine Lösung stellt sich auch wie von selbst ein.

Kommt das niemandem merkwürdig vor? One size fits all?

Ich bin ein großer Freund der Prinzipien Single Responsibility (SRP) und Separation of Concerns (SoC). Danach scheint mir ein bisschen viel Last auf den Schultern von TDD zu liegen.

Das ist es auch, was mich an Coreys Demonstration wieder stört. Er steht dabei nur als einer von vielen, die TDD zeigen. Die Vermischung von Lösung und Design stößt mir auf. Sie ist es nämlich, die zu den erwähnten Misserfolgen in den Dojos führt.

TDD bedeutete zunächst Test-Driven Development. Da ging es also um eine bestimmte Art zu codieren. Red-green-refactor. Das hat vor allem zu hoher Testabdeckung geführt.

Dann wurde aus dem Development das Design: Test-Driven Design. Die Betonung wurde damit auf den Refactoring-Schritt gelegt. Zur hohen Testabdeckung sollte dann auch noch eine “gut” Codestruktur kommen.

Und heute? Mir scheint, dass es gar nicht mehr um TDD geht, sondern um TDPS: Test-Driven Problem Solving. Nicht nur sollen Tests zu einem Design führen – denn über das Design soll man ja vorher nicht nachdenken, es soll entstehen, in minimaler Form. Nein, jetzt sollen am besten die Tests auch noch die Lösung herbeiführen.

Wenn Sie sich nun fragen, was denn da der Unterschied sei, dann rühren Sie genau an dem Problem, das ich meine: Es wird kein Unterschied zwischen Lösung und Code gesehen. Oder vielleicht sollte ich sagen, zwischen Lösungsansatz und Code? Ist es dann deutlicher?

Hier zwei Beispiele für den Unterschied.

Als erstes eine Lösung für das Problem des Sortierens eines Feldes. Text und Bild beschreiben einen Ansatz, sie beschreiben ein Vorgehen, sie sagen, wie man das Ziel ganz grundsätzlich erreichen kann:

image

Und jetzt ein Design in F# für diesen Lösungsansatz:

image

Im Design, im Code finden sich natürlich die Aspekte und Schritte des Lösungsansatzes wieder. Aber der Code ist nicht der Lösungsansatz. Er implementiert ihn in einer bestimmten Programmiersprache mit bestimmten Sprach- und Plattformmitteln.

Als zweites ein Lösungsansatz für ein Problem im Compilerbau, die Erkennung von Bezeichnern. Hier ein Syntaxdiagramm dafür:

image

oder alternativ ein Deterministischer Endlicher Automat:

image

Dass es überhaupt eine Phase zur Erkennung von Bezeichnern gibt (lexikalische Analyse), ist ebenfalls Teil eines Lösungsansatzes:

image

Das konkrete Code-Design, die Implementierung des Lösungsansatzes, könnte dann so aussehen:

image

Lösungsansatz – oder auch Modell – und Code in einer bestimmten Struktur – nach TDD auch Design genannt –, sind einfach verschiedene Aspekte. In den TDD-Vorführung wie bei Corey und den TDD-Selbstversuchen in den Dojos werden die jedoch nicht sauber getrennt. Immer wieder wird gehofft, durch Red-Green-Refactor nicht nur ein evolvierbares Design herzustellen, sondern auch eine Lösung zu bekommen.

Das (!) halte ich für falsch. Erstens ganz praktisch aus der Beobachtung heraus, dass so selten lauffähiger Code entsteht, der die Aufgabe erfüllt. Zweitens eher theoretisch aus dem Gedanken heraus, dass wir Menschen damit schlicht unterfordert sind. Das über Jahrtausende geschliffene Werkzeug “Denken” wird nicht genutzt. Man hofft vielmehr, durch Mustererkennung beim Code, irgendwie zu einer Lösung zu kommen.

Das funktioniert manchmal tatsächlich, wenn man genau hinschaut. Die Kata Roman Numerals könnte dafür ein Fall sein. Nur ist nicht zu erwarten, dass das immer so geht. Auf den Quicksort Lösungsansatz kommt man nicht durch TDD, davon muss man einfach eine Vorstellung entwickeln – eben einen Lösungsansatz. Im Kopf. Durch Nachdenken.

Und wie sollte es dann anders aussehen mit TDD?

Systematischeres Vorgehen

Systematisierung, Entzerrung, Entlastung, Fokus finde ich wichtig. Aus meiner Sicht sollte das Vorgehen diese Schritte beinhalten:

  1. Problem vorstellen
  2. Lösungsansatz entwickeln
  3. Testfälle ermitteln und priorisieren
  4. Lösungsansatz mit TDD implementieren

Schritte 2. und 3. können dabei mehrfach durchlaufen werden. Und wenn sich bei 4. noch neue Erkenntnisse zum Lösungsansatz ergeben sollten, dann ist das auch ok. Aber 4. ohne explizites 2. und 3. zu beginnen, halte ich für eine Überlastung.

Damit wären Lösungsansatz und Design getrennt. Damit würde – da bin ich ganz sicher – die Erfolgsquote jedes Coding Dojos steigen. Und wenn nicht, dann würde man genau sehen, woran es liegt: Liegt es an mangelnden TDD-Fähigkeiten oder liegt es an mangelndem Problemverständnis und dadurch fehlender Lösungsphantasie?

Die Single Responsibility von TDD liegt für mich bei der Testabdeckung und bei einer hohen Strukturqualität im Kleinen [2].

Was jedoch da überhaupt in Code gegossen und strukturiert werden soll… das ergibt sich nicht durch TDD, sondern durch Nachdenken. Den Lösungsansatz zu finden und die Testfälle zu priorisieren, das ist nicht Teil von TDD – muss aber gezeigt werden. Denn wird es nicht gezeigt bzw. wie bei Corey mit dem TDD-Vorgehen vermischt, entsteht entweder eine Überlastung, die die Aufgabenerfüllung behindert. Oder es entstehen “Wunderlösungen”, über die man nur staunen, sie aber eher nicht nachvollziehen kann. Wer “Wunderlösungen” goutiert, der wird es schwer haben, selbst Lösungen für andere Probleme zu finden.

Fußnoten

[1] Damit will ich nicht sagen, dass es keine Teams gibt, die die Aufgaben schaffen. Aber das passiert eben nicht regelmäßig und systematisch. Ich erinnere mich lebhaft an ein Dojo auf den XPdays und an eines auf der DDC im letzten Jahr. 5-10 Teams gab es jeweils. Und keines hat funktionierenden Code für die Kata Roman Numerals bzw. die Kata Zeckendorf (Übersetzung einer ganzen Zahl in eine Zeckendorf-Sequenz) abgeliefert.

[2] Wobei sogar diese Strukturqualität im Kleinen nicht einfach so entsteht, wie ich an anderer Stelle schon ausgeführt habe. Dafür braucht es Refactoring-Kompetenz und –Willen. Die kommen nicht aus TDD. Dafür braucht es aus meiner Sicht noch Hilfestellung, die Refactorings näher legt. Dadurch kann sich ein TDD 2.0 auszeichnen. Aber dazu ein andermal mehr…