Follow my new blog

Posts mit dem Label Flow-Design werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Flow-Design werden angezeigt. Alle Posts anzeigen

Dienstag, 27. Januar 2015

Die Selbstlern-Challenge - Mitmachen und gewinnen!

500€ gibt es zu gewinnen. Ingesamt jedenfalls. Die bin ich bereit, als “Preisgeld” auszusetzen für diejenigen, die erfolgreich mitmachen.

Die Herausforderung

Ich suche 10 Freiwillige, die bereit sind, 10 Wochen lang sich im Selbststudium mit Flow Design auseinanderzusetzen. Das habe ich in einigen Büchern und vielen Blog-Artikeln ausführlich beschrieben. Dazu gehören Themen wie:

  • Radikale Objektorientierung / OOP as if you meant it
  • Agile Architektur / Die Anforderung-Logik-Lücke
  • Softwarezellen

Über die 10 Wochen ist nicht viel zu leisten. Es gibt nur drei Bedingungen:

  1. Wer teilnimmt, stellt mir jede Woche mindestens eine ernstgemeinte und nicht triviale Frage zu den obigen Themen. Diese Frage soll als minimale Dokumentation der kontinuierlichen und fortschreitenden Auseinandersetzung mit dem Stoff dienen. “Fragen auf Vorrat” sind nicht erlaubt, ebensowenig “aufholende Fragen”, die eine Woche ohne Frage kompensieren sollen.
  2. Wer teilnimmt, reagiert auf Emails von mir zum Lernstoff innerhalb von max. 24h. Dies dient der Sicherung einer verlässlichen, flüssigen Kommunikation zur Beförderung des Lernens.
  3. Wer teilnimmt, stimmt zu, dass sein/ihr Name veröffentlicht wird. Außerdem behalte ich mir vor, die Abschnitte zum Lernstoff aus der sich ergebenden Email-Konversation zu veröffentlichen.

Das ist alles. Es gibt keine Prüfung, es gibt überhaupt nichts, das am Ende abzuliefern wäre. Der Weg ist das Ziel :-)

Dieses Angebot gilt vom 27.1.2015 bis zum 17.5.2015 (oder anders: ab 5. KW bis inklusive 20. KW 2015). In der Zeit muss die 10-Wochen-Teilnahme abgeschlossen sein.

Update: Seit 28.1.2015 sind alle Teilnehmerplätze vergeben!

Die Teilnahme

Wer teilnehmen möchte, schreibt mir einfach eine Email. Dann beginnen die 10 Wochen zu laufen. Ich stelle daraufhin Lernmaterialien in Form von Links und Büchern zur Verfügung. Teilnehmer müssen also keinen Cent ausgeben.

Auf Wunsch stelle ich auch Aufgaben, an denen sich Teilnehmer versuchen können.

Der Gewinn

Der Gewinn der Teilnahme besteht natürlich vor allem in Wissens- und Erfahrungszuwachs. Darüber hinaus erhält aber auch jeder erfolgreiche Teilnehmer am Ende noch einen Amazon-Gutschein in Höhe von 50€.

Erfolgreich ist, wer die obigen Bedinungen erfüllt. Mehr ist nicht nötig, weniger allerdings auch nicht.

Die Hypothese

Mit dieser Herausforderung möchte ich eine Hypothese überprüfen, die ich zum autodidaktischen Lernen habe. Welche das ist, kann ich vorher nicht verraten. Damit würde ich die Durchführung des Experiments beeinflussen. Aber ich verspreche, dass ich am Ende der Herausforderung darüber berichte.

 

Und nun: Lasst das Selbstlernen beginnen!

Dienstag, 10. Dezember 2013

Vergleich von Flow-Designs für Kata Ordered Jobs

So unterschiedlich können Flow-Designs sein. Ich hatte es schon geahnt, doch nun ist es öffentlich.

Am 1.11.2013 hatte ich ermuntert, Flow-Designs für die Kata Ordered Jobs “einzureichen”. Auslöser dahinter war ein Flow-Design, dass Entwickler eines Kunden von mir in einem Coding Dojo unter sich erarbeitet hatten. Sie wollten einfach Flow-Design üben. Das sah so aus, als sie mich schließlich um Rat fragten:

image

Was mich daran überraschte: Dem Lösungsansatz ist nicht anzusehen, um welches Problem es eigentlich geht. “Process”, “Release”, “Save”, “Output” sind ganz allgemeine Begriffe. Auch der fließende “Job” erhellt nicht wirklich, wie (!) die Lösung funktioniert. Die Musik spielt in “Process” – aber genau die Funktionseinheit wurde nicht verfeinert.

Ob das auch anderen Interessenten an Flow-Design so geht/gehen würde? Das wäre schade, denn ich glaube, so führt Flow-Design zu keiner großen Verbesserung der Verständlichkeit und Evolvierbarkeit von Software.

Es ist ja gerade der Trick an Flows, dass bei ihnen die Syntax so minimal ist, dass sie eine Domänensemantik nicht verrauscht.

Fünf “Einsendungen” hat es dann auf meinen Aufruf hin gegeben. Vielen Dank an die fleißigen Entwerfer! Die möchte ich hier allerdings nicht in ihrer Funktionstauglichkeit bewerten, sondern nur formal gegenüberstellen. Wie sich zeigt, gibt es nämlich mehrere Aspekte, in denen sie sich unterscheiden.

Darstellung

Flow-Design setzt auf… Flüsse. Der Name ist Programm. Die können handgemalt sein wie oben oder in einem Tool wie Visio ordentlich “gesetzt” werden:

image

Überrascht hat mich daher die folgende Darstellung:

image

Aber nicht wegen der “squiggly lines” bin ich überrascht, sondern wegen der Gewichtung der Darstellung. Hier sind zwar auch Pfeile im Spiel und stehen für Datenflüsse. Doch prominenter scheinen mit die Abhängigkeiten. Da ist ein DGraph von mehreren Flow-Funktionseinheiten abhängig. Oder? Nein, umgekehrt. Jetzt sehe ich es: Die Funktionseinheiten sind vom DGraph abhängig. Ich war nur einen Moment verwirrt, weil die Abhängigkeiten von unten nach oben zeigen. Das ist eher ungewöhnlich (in Flow-Designs allemal), finde ich.

Im oberen Entwurf gibt es auch eine Abhängigkeit (zu “nextOrderNumber”). Doch die ist zurückhaltender dargestellt; sie steht dem Fluss nicht im Weg.

Die Darstellung bzw. Gewichtung von Abhängigkeiten unterscheidet sich also in Flow-Designs durchaus. Das ist bis zu einem gewissen Grad auch ok, finde ich. Doch wenn die Abhängigkeiten scheinbar die Oberhand gewinnen, wenn der Fluss nicht mehr im Vordergrund steht, dann finde ich einen Vorteil von Flow-Design verschenkt.

Detaillierungsgrad

Wie das erste Diagramm zeigt, können Flow-Designs sehr unspezifisch sein. “Process” passt auf nahezu jedes Problem. Ähnlich empfinde ich aber auch noch bei “Sort” im dritten Diagramm – zumindest solange dann nicht weiter verfeinert wird. So bleibt die Lösung im Dunkeln; die Last liegt voll auf der Codierung und ist damit die Verantwortlichkeit nur eines Entwicklers.

Dito im folgenden Entwurf, wo noch klarer wird, dass die Aufgabe eigentlich nicht vom Flow, sondern von der Datenstruktur DGraph gelöst wird. So eine Datenstruktur samt Algorithmus zur topologischen Sortierung “einzukaufen”, ist natürlich legitim. In der Praxis würde man es womöglich sogar anstreben, um sich eigenen Aufwand zu sparen. Doch das funktioniert natürlich nur, wenn es eine Lösung “einzukaufen” gibt. Und hier war es an der Aufgabe vorbei, da es ja gerade darum ging, selbst einen Lösungsansatz zu formulieren.

image

Andererseits können Flows natürluch auch sehr detailliert sein:

image

In diesem und weiteren Flows der “Einreichung” wird der Algorithmus genau beschrieben. Jede Funktionseinheit wird mit 2-3 Zeilen Code umsetzbar sein, schätze ich. Für mich ist das sogar zu detailliert, zumindest für die Implementierung. Bei Entwurf mag man mal so tief einsinken… doch dann würde ich wieder einige Ebenen nach oben steigen beim Codieren. Nicht für jede Funktionseinheit lohnt wirklich eine eigene Funktion.

Lösungen können also zu grob sein, dann helfen sie nicht wirklich. Andererseits können sie auch zu detailliert sein, dann fangen sie an zu rauschen. Die Kunst besteht mithin darin, eine angemessene mittlere Granularität zu finden. Die ist natürlich ein Stück subjektiv und erfahrungsabhängig ;-)

Abstraktion

Flows entwerfen, ist etwas anderes als zu codieren. Es geht um Abstraktion von den Niederungen der textuellen Programmiersprachen. Nur dadurch gewinnt der Entwurf Geschwindigkeit und Überblickscharakter. Es geht darum, eine Karte zu schaffen, nicht eine (Miniatur)Landschaft zu herzustellen.

Auch hier gilt es, die Balance zu finden. Am einen Ende des Spektrums ist der Globus, d.h. eine sehr grobe Karte, die alles zeigt. Das ist völlig ok, nein, unumgänglich – sollte nur nicht wie im vorletzten Diagramm dabei bleiben. Die schrittweisen Verfeinerungen, die auch im Code erhalten werden können und sollen, machen das Flow-Design aus.

imageimageimageimage

In die falsche Richtung geht es aus meiner Sicht jedoch, wenn sich in die Karten Artefakte aus dem Terrain einschleichen. Das ist der Fall, wenn der Fluss nicht mehr deklarativ ist, sondern imperativ wird. Das ist hier z.B. der Fall:

image

Ein “ForEach” ist beim Flow-Design fehl am Platze; der Begriff steht erstens für eine explizite Kontrollanweisung, wo es doch um Datenflüsse geht, und zweitens ist er frei von jedem Domänenbezug.

Dicht darauf folgen Schleifen jeder Art, z.B.

image

Sie lassen sich einfach nicht mit dem Integration Operation Segregation Principle (IOSP) in der Implementation mit 3GL vereinbaren.

Ein Flow-Designs beschreiben Datenflüsse, dazu gehören mindestens zwei Punkte, um eine Strecke für den Fluss abzustecken und ihm eine Richtung zu geben. Andererseits dürfen die Flüsse aber auch nicht umkippen und zu Kontrollflüssen degenerieren. Abstraktion im Entwurf bedeutet also, soweit wie möglich eine deklarative Domänensprache zur Lösung eines Problems zu finden.

Anforderungstreue

Überrascht hat mich schließlich auch die Kreativität der Lösungen. Die Vorgaben waren ja klar. Es galt, ein Interface zu implementieren.

Doch das hat nicht in allen Lösungen dazu geführt, auch dieses Interface sichtbar zu machen. Manchmal wurde aus dem geforderten Register() nur ein AddJob(). Manchmal war solch eine Funktionalität aber auch gar nicht zu sehen:

image

image

Am anderen Ende des Spektrums dann wieder eine recht treue Abbildung der Anforderung:

image

Kreativität ist bei der Findung eines Lösungsansatzes selbstverständlich wichtig. Allerdings sollte sie sich dort austoben, wo die Anforderungen notwendig Lücken haben. Das, was der Kunde beschreibt, das, was klar erkennbar ist, sollte hingegen treu übernommen werden. Sonst leidet die Verständlichkeit. Man bedenke immer den anderen Entwickler oder sich selbst in der Zukunft. Da wird dann U in den Anforderungen gelesen, aber im Entwurf ist ein X zu sehen…

Ich weiß, das ist manchmal schwer. Da hat man seine Coding-Standards. Da wallt die Erfahrung in einem auf. Das will alles berücksichtigt werden. Doch ich glaube, wir tun gut daran, uns solchen Regungen nicht gleich hinzugeben. Etwas Disziplin in der Beschränkung der Kreativität hilft der Verständlichkeit.

Fazit

Dass die Entwürfe so weit auseinander gehen, hätte ich doch nicht gedacht. Auch mit den überschaubaren Mitteln des Flow-Design ist also große Bandbreite möglich. Einerseits schön – andererseits aber auch wieder eine ernüchternd. Die guten alten Tugenden Maßhaltung und Genauigkeit und Fokussierung gelten auch im Flow-Design.

Mir hat der Vergleich der Entwürfe sehr geholfen. Und ich hoffe, dass es Ihnen ein bisschen auch so geht.

Freitag, 1. November 2013

Mitmachen: Flow-Designs gesucht

Für Programmieraufgaben gibt es ja nicht nur eine Lösung. Aber wie bunt die Lösungswelt ist, ist andererseits auch nicht ganz klar. Deshalb würde ich mich freuen, wenn Sie mitmachen würden bei der Sammlung von Lösungen.

Zu einem kleinen Programmierproblem möchte ich Lösungen in Form einer kleinen Bilderausstellung veröffentlichen. Dann können wir darüber diskutieren. Allerdings geht es mir nicht um Quellcode, sondern um visuelle Lösungen.

imageAlso:

Ich suche Lösungen zum Problem “Ordered Jobs” in Form von Flow-Designs.

Datenflüsse sollen im Mittelpunkt stehen. Aber wer die mit weiteren Bildern garnieren möchte, ist dazu herzlich eingeladen.

Die Ausdrucksmittel des Flow-Designs habe ich in vielen Publikationen beschrieben und über die Jahre verfeinert. Leider bin ich noch nicht dazu gekommen, einen aktuellen Stand an einem Ort zusammenzufassen. Mea culpa. Aber es gibt einen Spickzettel, der größtenteils aktuell ist und die Notation knapp beschreibt. Alternativ beschreiben einige Artikel unter der Überschritz “OOP as if you meant it” nochmal in Kürze meine aktuelle Sicht zu Flow-Design.

Wer mitmachen will, schreibt am besten einen Kommentar zu diesem Artikel mit einem Link zu einem PDF mit seinem Entwurf. Oder es können Links zu yuml.me Grafiken sein; mit den Aktivitätsdiagrammen kann man Flow-Designs recht ordentlich beschreiben. Oder schicken Sie mir eine Email mit einem PDF.

Am Ende bastle ich dann eine Zusammenschau mit ein paar Anmerkungen zu dem, was mir auffällt. Und ich lege auch meine Vorstellung von einer Lösung vor. Dann gibt es eine “Ausstellungseröffnung” hier im Blog :-)

Zu gewinnen gibt es nichts – außer Erkenntnissen.

Wer macht mit? Einsendeschluss ist der 30.11.2013.

Ich bin gespannt auf die Vielfalt der Lösungsansätze.

Und hier nun die Aufgabe in epischer Länge. Sie ist auch im Fundus der Programmier-Katas der CCD School zu finden.

Class Kata Ordered Jobs by Clean Code Developer School

Dienstag, 24. September 2013

Bye, bye, function. Hello, transformer!

Funktionen sollten nicht länger die kleinsten Bausteine unseres Codes sein. Sie vermengen nämlich zwei Aspekte: Kontrollfluss und Datenfluss.

Wenn eine Funktion die Kontrolle aufgibt, dann fließen Daten aus ihr heraus. Sie kann danach die Kontrolle nicht zurückbekommen. Erst ein erneuter Aufruf überträgt Kontrolle wieder an sie.

Wenn Daten aus einer Funktion fließen sollen, dann muss sie die Kontrolle aufgeben. Sie kann keine Zwischenergebnisse zur Weiterverarbeitung liefern.

Kontrolle und Daten fließen mit Funktionen also immer gleichzeitig. Das ist oft ok – aber eben nicht immer. Indem wir diese Aspektkopplung jedoch so tief in unseren Sprachen verankert haben, fällt es uns schwer, anders zu denken. Das halte ich aber für nötig, wenn wir evolvierbarere Software herstellen wollen.

Funktionen sind ein Relikt aus der Anfangszeit der Programmierung. Sie sind syntactic sugar für Maschinencodebefehle, die nicht nur ein Unterprogramm aufrufen (CALL), sondern auch noch anschließen ein Resultat für den Aufrufer bereitstellen. Dabei gibt es immer nur einen Punkt, an dem die Kontrolle ist: den Befehl, auf den der eine Befehlszeiger weist. Daten sind in diesem Paradigma wie Hunde, die ihrem Herrchen an der Leine folgen müssen. Sie können immer nur am selben Ort in Verarbeitung gedacht werden, wo auch gerade die eine Kontrolle ist.

Das ist alles wunderbar und verständlich. Das hatte seine lange Zeit. Doch ich glaube, wir sollten jetzt darüber hinaus gehen. Dadurch wird dieses Paradigma nicht überflüssig. Die Newtonsche Physik gilt ja auch weiterhin. Aber wir denken dann nicht mehr, dass alles nur mit diesen Mitteln erklärt und beschrieben werden muss. Die relativistische Physik umfasst die Newtonsche. So sollte auch ein neues Paradigma das bisherige umschließen.

Ist da schon alles mit der Funktionalen Programmierung gesagt? Hm… Nein, ich glaube, nicht. In der steckt ja schon im Namen die Funktion, also das, was wir überwinden sollten.

Aber wie können Kontrollfluss und Datenfluss entkoppelt werden? Mit Continuations bzw. Observern.

Aus einer Funktion

R f(P p) {
  …
  return r;
}

würde z.B. eine Prozedur wie

void f(P p, Action<R> continueWith) {
  …
  continueWith(r);
}

Diese Prozedur hat alle Freiheiten, Kontroll- und Datenfluss zu entkoppeln:

  • Sie kann ein Resultat via continueWith() liefern und dann die Kontrolle aufgeben – oder auch nicht.
  • Sie kann entscheiden, überhaupt ein Resultat zu liefern.
  • Sie kann sogar entscheiden, mehrfach ein Resultat zu liefern.
  • Und schließlich ist eine solche Prozedur auch nicht darauf festgelegt, nur über einen “Kanal” Daten zu liefern.

void f(P p, Action<R> onR, Action<T> onT) {
  …
  onR(r);
  …
  onT(t);
  …
}

Solange der Name der Continuation auf die Prozedur bezogen ist, erhält sie keine Information über den Kontext der Weiterverarbeitung ihres Output. Wie eine Funktion erfüllt sie damit das Principle of Mutual Oblivion (PoMO).

Ich nenne so ein Unterprogramm Transformator. Funktion impliziert gleichzeitigen Kontroll- und Datenfluss. Transformator ist als Begriff hingegen noch nicht verbrannt. Und irgendetwas gibt es ja immer zu transformieren, oder? Zahlen in andere Zahlen, Zeichenketten in andere Zeichenketten oder Zahlen in Zeichenketten oder Zeichenketten in Wahrheitswerte oder in-memory Daten in persistente Daten usw. usf.

Unsere Programmiersprachen sind natürlich für den Umgang mit Funktionen optimiert:

var y = f(x);
var z = g(y);
h(z);

Das lässt sich leicht hinschreiben und lesen. Aber leider ist es begrenzt in seiner Ausdrucksfähigkeit, weil eben Kontrolle und Daten immer gleichzeitig fließen müssen.

Die Alternative mit Transformatoren ist nicht so schön, ich weiß:

f(x, y =>
g(y,
h));

Mit ein wenig Übung kann man das allerdings auch flüssig lesen und hinschreiben. Aber es ist zumindest ungewohnt. Schöner wäre es, wenn man z.B. schreiben könnte:

f –> g –> h

In F# geht das ähnlich – allerdings nur für Funktionen. Bei aller Fortschrittlichkeit ist man auch dort dem alten Paradigma verhaftet. Es sitzt tief in uns.

Wie eine Lösung aussehen kann, weiß ich auch nicht. In meiner Entwicklung von Funktionen hin zu Transformatoren möchte ich mich dadurch aber nicht beschränken lassen. Denken kann ich Transformatoren, visuell darstellen kann ich Transformatoren, codieren kann ich Transformatoren. Da wird sich doch auch eine textuelle Notation finden lassen, oder?

Der Pfeil scheint mir ein passender Operator, wenn mit Transformatoren nun Datenflüsse in den Blick kommen. Vielleicht könnten dann mehrere Flüsse so notiert werden:

f.onR –> g.onY –> h,
f.onT –> m,
g.onZ –> n;

Das Diagramm dazu wäre:

image

Das ist wunderbar übersichtlich. Der heutige C#-Code hinkt leider hinterher:

f(x, r =>
  g(r,
    h,
    n),
  m);

Aber ich bin unverdrossen. Den Übergang von Funktionen zu Transformatoren halte ich für wichtig. Wir lösen uns damit aus der Umklammerung der von-Neumann-Maschinen. Nicht Kontrolle ist wichtig, sondern Resultate, d.h. der Fluss von Daten.

Kontrolle kann in mehreren Transformatoren gleichzeitig sein. Oder Kontrolle kann vor uns zurück springen, wenn sie denn nur an einem Ort zur Zeit sein kann.

Die Funktion als Werkzeug wird damit nicht überflüssig, sondern nur als Sonderfall neu positioniert. Transformatoren können sich wie Funktionen verhalten, wenn es sein muss. Können, müssen aber nicht. Und das find ich so wichtig. Mit Transformatoren ist entkoppelt, was nicht zwangsläufig zusammengehört. Freiheit für die Daten!

Sonntag, 28. April 2013

Software fraktal - Funktionale Abhängigkeit entschärfen

Wir kommen nicht wirklich raus aus der funktionalen Abhängigkeit. Was aber können wir dann gegen ihre Probleme tun?

Neulich habe ich gezeigt, wie Sie funktionale Abhängigkeiten auflösen. Dabei bleibe ich auch. Funktionseinheiten - Methode oder Klasse - sollten für ihre Spezifikationen selbst verantwortlich sein. Soweit möglich. Doch damit wird das Problem der funktionalen Abhängigkeit nur eine Ebene höher geschoben.

Vorher:
int c() {
  ...  // Vorbereitung
  var y = s(x);
  ... // Nachbereitung
}

Nachher:
var x = c_vorbereitung();
var y = s(x);
return c_nachbereitung(y);

Wo c() vorher funktional von s() abhängig war, da ist das, was c() ausgemacht hat nachher auf c_vorbereitung() und c_nachbereitung() verteilt - und nicht mehr von s() abhängig.

In Bezug c() ist das gut. Aber nun ist der Code, der nachher die drei Methoden aufruft funktional von ihnen abhängig. Denn in Wirklichkeit sieht das ja so aus:

Vollständiges Nachher:
int i() {
  var x = c_vorbereitung();
  var y = s(x);
  return c_nachbereitung(y);
}

Die neue Methode i() hat den schwarzen Peter funktionale Abhängigkeit zugeschoben bekommen.

Oder?

Ich denke, wir sollten genau hinschauen, bevor wir die Situation als unverändert beurteilen.

Operationen

Ich möchte für die weitere Diskussion den Begriff Operation einführen. Eine Operation ist eine Funktion, die Logik enthält. Das bedeutet, sie enthält Kontrollstrukturen (if, for, while usw.) und/oder Ausdrücke (+, *, && usw.).

Die Funktion

int Add(int a, int b) { return a+b; }

ist nach dieser Definition eine Operation. Die obige Funktion i() hingegen nicht. Sie enthält keine Kontrollstrukturen und auch keine Ausdrücke; sie besteht lediglich aus Funktionsaufrufen und Zuweisungen [1].

Operationenhierarchien

Normalerweise findet die Verarbeitung in Operationenhierarchien statt. Es gibt tiefe funktionale Abhängigkeitsbäume und in jedem Knoten (Funktion) befindet sich Logik.

void s(int a) {
  var n = ... a ...;
  var l = t(n);
  foreach(var e in l) { ... }
}

List t(int n) {
  var l = new List();
  for(int i=0; i<n; i++)
    l.Add(u(i));
  return l;
}

int u(int i) {
  var x = ... + i * ...;
  var y = v(x);
  return ... - y / ...;
}

...

Die Logik auf jeder Ebene mag derselben Domäne angehören oder unterschiedlichen. Das ist egal. Es ist Logik. Und solche Logik gemischt mit funktionaler Abhängigkeit tendiert dazu, sich horizontal auszudehnen (vgl. Problem #3 in diesem Artikel). Das bedeutet, tendenziell sind die Operationen auf jeder Ebene umfangreich und somit nicht leicht zu verstehen [2].

Operationenhierarchien leiden deshalb unter einigen Problemen:

Problem #A: Grenzenloses Wachstum

Operationenhierarchien kennen keine Grenze beim Wachstum. In Breite und Tiefe können sie beliebig zunehmen. Neue Anforderungen oder auch gut gemeinte Refaktorisierungen führen zur Ausdehnung - und verschmieren damit Logik immer mehr. Logik kann ja auf jeder Ebene der Hierarchie in jedem Operationsknoten vorkommen.

Spüren Sie nicht, wie schwer es dadurch wird, Software überhaupt nur zu verstehen? Um nur irgendwie noch durchzublicken, müssen Sie Tools bemühen, die Ihnen Abhängigkeitsbäume darstellen. Nur so bekommen Sie ungefähr eine Idee davon, wo bestimmte Logik sitzen könnte.

Problem #B: Umfängliche Unit Tests auf jeder Ebene

Operationen sind die Orte, wo die Musik einer Software spielt. Also müssen sie solide getestet werden. Jeder Pfad durch sie sollte mit einem automatisierten Test abgedeckt sein. Deshalb ist ja auch TDD in aller Munde. Deshalb gibt es Tools, die die Code Coverage messen.

Wenn Code eine endlose Operationenhierarchie ist, müssen solch aufwändige Tests für jeden Knoten durchgeführt werden. Puh... Und dann enthalten diese Knoten auch noch funktionale Abhängigkeiten. Es gibt also noch ein weiteres Problem:

Problem #C: Attrappen auf jeder Ebene

Auf jeder Ebene einer Operationenhierarchie müssen funktionale Abhängigkeiten durch Attrappen ersetzt werden. Sonst wird nicht nur eine Unit, d.h. eine Operation getestet. Kein Wunder, dass es soviele Mock Frameworks gibt. Der Bedarf an Attrappen ist durch Operationenhierarchien riesig.

Integration

Operationen machen viel "Dreck". Den muss man mit viel Clean Code Development dann wieder wegräumen. Schauen Sie zum Vergleich nun aber noch einmal die obige Funktion i() an. Finden Sie die "dreckig"?

Ich nicht. Für mich ist i() ein Leuchtturm an Sauberkeit. Was i() wie leistet, ist ganz einfach zu erkennen. Zugegeben, das Beispiel ist abstrakt. Nehmen wir deshalb ein konkreteres:

string Erste_Seite_aufblättern() {
  var dateiname = Dateiname_von_Kommandozeile_holen();
  var alleDatenzeilen = Zeilen_lesen(dateiname);
  var ersteSeite = new Seite {
    Überschrift = Überschrift_extrahieren(alleDatenzeilen),
    Datenzeilen = Zeilen_der_ersten_Seite_selektieren(alleDatenzeilen)
  };
  return Seite_formatieren(ersteSeite);
}

Das ist ebenfalls keine Operation. Es passiert zwar einiges, doch Kontrollstrukturen und Ausdrücke sind abwesend. Alles, was passiert, steckt in Funktionen, die aufgerufen werden.

Die Funktion Erste_Seite_aufblättern() ist also stark funktional abhängig - dennoch ist sie leicht zu verstehen. Woran liegt das?

Hier wurde das SRP sauber beachtet. Diese Funktion hat wirklich nur eine Verantwortlichkeit: sie integriert. Sie tut nichts im Sinne einer Domänenlogik selbst, sondern delegiert ausschließlich. Ihre einzige Aufgabe ist es, Funktionen mit kleinerem Zweck zu einem größeren zusammenzustellen. Sie stellt eine Integration dar.

Operationen hingegen vermischen immer zwei Verantwortlichkeiten, wenn sie funktional abhängig sind. Ihr Hauptverantwortlichkeit ist die Ausführung von Logik. Neben der übernehmen sie jedoch auch noch integrierende Verantwortung, wenn sie zwischendurch andere Funktionen aufrufen. Das macht sie so schwer zu verstehen und zu testen.

Trennung von Integration und Operation

Für mich ergibt sich aus den Problemen der Operationshierarchien und der Probleme der funktionalen Abhängigkeit, dass wir etwas anders machen müssen, als bisher. Ein bisschen SOLID mit TDD reicht nicht. Wir müssen radikaler werden.

Ich glaube, besser wird es nur, wenn wir ganz klar Integrationen von Operationen trennen (Integration Operation Segregation Principle (IOSP)). SPR und das Prinzip Single Level of Abstraction (SLA) sind nicht genug. Sie führen nicht geradlinig genug zu verständlichen Strukturen.

Um Hierarchien von Funktionen kommen wir nicht herum. Ohne sie beherrschen wir nicht-triviale Logik nicht. Wir müssen Logikblöcke "wegklappen" und wiederverwendbar definieren können. Dafür sind Unterprogramme (Prozeduren und Funktionen) nötig.

Aber wir können uns darin beschränken, wie wir diese Hierarchien aufbauen. Wir können uns auferlegen, dass Operationen nur in ihren Blättern stehen dürfen - mit beliebig vielen Ebenen von Integrationen darüber.

Jede der beliebig vielen Integrationsfunktionen oberhalb von Operationen ist dann so gut zu lesen wie Erste_Seite_aufblättern().

Die Probleme funktionaler Abhängigkeit wären in den Integrationen zwar noch relevant; doch sie wären auf ein erträgliches Maß gedämpft:

Bereitstellungsaufwand wäre zu treiben (Problem #1) und es gäbe topologische Kopplung (Problem #2). Aber das grenzenlose Wachstum (Problem #3) sähe anders aus. Integrationsfunktionen mit vielen LOC wären viel, viel besser zu lesen als Operationen von gleicher Länge. Entstünden lange Integrationsfunktionen aber überhaupt? Nein. Die Möglichkeit zum grenzenlosen Wachstum gibt es zwar, doch da Integrationsfunktionen so übersichtlich sind, werden sie viel schneller refaktorisiert.

Wenn Sie in Erste_Seite_aufblättern() zwei Verantwortlichkeiten vermischt sehen (Widerspruch gegen SRP) oder meinen, da sei nicht alles auf dem selben Abstraktionsniveau (Widerspruch gegen SLA), dann klappen Sie einfach einen Teil weg in eine andere Integrationsfunktion:

string Erste_Seite_aufblättern() {
  var ersteSeite = Erste_Seite_laden();
  return Seite_formatieren(ersteSeite);
}

Seite Erste_Seite_laden() {
  var dateiname = Dateiname_von_Kommandozeile_holen(); 
  var alleDatenzeilen = Zeilen_lesen(dateiname);
  return new Seite {
    Überschrift = Überschrift_extrahieren(alleDatenzeilen),
    Datenzeilen = Zeilen_der_ersten_Seite_selektieren(alleDatenzeilen)
  };
}

Aus eins mach zwei. Das kann jedes Refactoring-Tool, das etwas auf sich hält.

Dadurch steigt zwar die Tiefe der Funktionshierarchie, doch das macht nichts. Jede Ebene für sich ist ja leicht verständlich. Sie brauchen nicht mal ein Werkzeug, das Ihnen Abhängigkeitsbäume zeichnet, da die Ihnen ja deutlich vor Augen stehen und nicht in "Logikrauschen" verborgen sind.

Auch die syntaktische (Problem #4) und semantische Kopplung (Problem #5) verlieren an Gewicht. Da Integrationen selbst keine Logik enthalten, haben Änderungen in Syntax oder Semantik keinen Einfluss auf die Integration selbst. Syntaktische Änderungen können durch Typinferenz womöglich verschluckt werden. Und wenn nicht, dann hilft wahrscheinlich ein kleiner Einschub zwischen den aufzurufenden Funktionen wie hier gezeigt.

Integrationen sind zwar grundsätzlich gerade an die Semantik der Funktionen gekoppelt, von denen sie abhängen. Sie sollen ja aus ganz konkreten Teilverantwortlichkeiten eine neue Summenverantwortlichkeit herstellen. Doch diese Kopplung ist loser, weil die nicht an eigener Logik hängt.

Soweit die Milderung der Probleme #1..#5 funktionaler Abhängigkeit durch das IOSP. Aber was ist mit den Problemen #A..#C der Operationenhierarchien?

ad Problem #A: Einzelne Integrationen können zwar wachsen, doch tendenziell tun sie das viel weniger als Operationen (s.o.).

Integrationshierarchien können auch wachsen und tun das durch Refaktorisierungen womöglich sogar mehr als Operationshierarchien. Doch das ist nicht schlimm. Jede Ebene ist einfach zu verstehen. Logik ist nicht vertikal und horizontal verschmiert. Sie steckt allein in den Blättern, den Operationen.

ad Problem #B: Integrationen haben von Hause aus eine sehr niedrige zyklomatische Komplexität. Es gibt nur wenige Pfade durch sie. Die kann man sehr einfach testen. Oft reicht ein einziger Test. Oder Sie lassen eine Integration auch mal ganz ohne Test.

Das meine ich ernst. Da Integrationen so leicht zu verstehen sind, kann ihre Korrektheit auch mal nur durch einen Code Review geprüft werden. Die Korrektheit besteht ja nur darin, dass eine Integration von den richtigen Funktionen abhängt und zweitens diese Funktionen in passender Weise für den Zweck der Integration "verdrahtet" sind. Ob das der Fall ist, ergibt sich oft durch Augenschein.

Allerdings sollten Sie automatisierte Tests der Integrationen auf oberster Ebene haben. Die prüfen den Gesamtzusammenhang. Solange der korrekt ist, sind auch darunter liegende Integrationen korrekt.

Und Sie decken natürlich Operationen mit automatisierten Tests ordentlich ab. Dort spielt die Musik der Logik, das ist nicht einfach zu verstehen, also müssen Tests helfen.

Mein Gefühl ist, dass das Testen von Funktionshierarchien, die dem IOSP folgen, deutlich einfacher ist. Allemal, da die Operationen ja nicht mehr voneinander abhängig sind. Sie sind durch das Principle of Mutual Oblivion (PoMO) entkoppelt.

ad Problem #C: Nicht zuletzt ist das Testen einfacher, weil der Bedarf an Einsätzen eines Mock Frameworks deutlich sinkt. Ich zum Beispiel benutze schon lange gar keinen mehr.

Attrappen sind hier und da noch nötig. Doch dann schnitze ich sie mir kurz für den konkreten Fall selbst. Wissen über einen Mock Framework vorzuhalten oder gar deren Entwicklung zu verfolgen, wäre viel umständlicher.

Warum sind Attrappen viel seltener nötig? Weil die vielen Tests von Operationen keine mehr brauchen. Sie sind ja in Bezug auf die Domäne ohne funktionale Abhängigkeit. Und weil Tests von Integrationen seltener sind.

Skalieren mit dem IOSP

Dass Sie Ihre Software nur mit einer Hierarchie von Integrationen realisieren können, an denen unten wie Weihnachtsbaumkugeln Operationen baumeln, glaube ich auch nicht. Aber ich denke, dies sollte die Grundstruktur sein:


Operationen sind die Black Boxes an der Basis funktionaler Abhängigkeitshierarchie. Nur dort sollte sich Logik befinden. Hier gilt es ausführlich zu testen. Diese "Büchsen" wollen Sie so selten wie möglich öffnen. Besser ist es, auf einer Integrationsebene darüber simple Veränderungen anzubringen und neue "Büchsen" unten hinzuzufügen.

Für die ganze Struktur gilt das IOSP, für jeden Knoten darin das PoMO.

Wenn diese Grundstruktur jedoch wachsen soll, dann reicht es nicht, an der Basis in die Breite zu gehen und bei der Integration in die Höhe. Ich denke, dann muss Schachtelung dazukommen.

Black Boxes sind nur aus einem bestimmten Blickwinkel oder aus einer gewissen Entfernung undurchsichtig und quasi von beliebiger Struktur. Wenn eine Operation wächst, dann sollten Sie das ebenfalls nach dem IOSP tun.


Software ist also von ähnlich fraktaler Struktur wie diese quadratischen Koch-Kurven:



Fazit

IOSP + PoMO sind für mich eine ganz pragmatisches Paar, um Software verständlich, testbar und evolvierbar zu halten.

Sie mögen einwänden, das ließe sich doch auch mir SRP und SLA und OCP (Open-Closed Principle) erreichen - und da haben Sie wohl recht. Nur passiert das nicht. SOLID allein reicht nicht. Deshalb finde ich es sinnvoll, daraus diese zwei Prinzipien zu destillieren. Oder diese simplen Regeln, wenn Sie wollen:
  • Teile Funktionen klar in Operationen und Integrationen:
    • Halte Funktionen, die Logik enthalten, frei von funktionalen Abhängigkeiten (Operationen).
    • Sammle funktionale Abhängigkeiten in Funktionen ohne Logik (Integration).
Wenn Sie Ihren Code so aufbauen, dann fühlt sich das zunächst natürlich ganz anders an als bisher. Sie werden Widerstand spüren. Und schon vorher werden Sie wahrscheinlich daran zweifeln, ob das alles überhaupt nötig sei.

Keine Sorge, das kenne ich alles. Ich bin selbst durch diese Zweifel gegangen. Ich kann Sie davon mit Argumenten auch nicht völlig befreien. Sie müssen das ausprobieren. Nehmen Sie sich eine Aufgabe vor, deren Code Sie einfach mal nach der Regel versuchen zu strukturieren. Eine Application Kata hat da gerade die richtige Größe [3].

Ebenfalls kann ich Ihr Schmerzempfinden nicht sensibilisieren. Wenn Sie heute darunter stöhnen, dass Ihre Codebasis schwer wartbar sei, aber keinen Schmerz empfinden, wenn Sie nur mit SOLID und TDD bewaffnet daran herumwerkeln, dann ist das halt so. Dann werden Sie in IOSP+PoMO als Extremformen von SPR, SLA und OCP keine Perspektive zur Besserung sehen. Aber vielleicht lesen Sie dann mal Watzlawicks "Vom Schlechten des Guten" :-)

Ich jedenfalls arbeite seit einigen Jahren zunehmend und heute nur noch so. Mein Leben ist dadurch glücklicher geworden :-) Ich kann meinen Code auch noch nach Monaten verstehen. Änderungen fallen mir leichter. Mit Mock Frameworks muss ich mich nicht mehr rumschlagen.

Versuchen Sie es doch einfach auch mal. Nur so als Experiment. Und dann sagen Sie mir, wie es Ihnen damit gegangen ist.

Endnoten

[1] Zuweisungen sind keine Ausdrücke, da nichts zu einem neuen Ergebnis kombiniert wird. Auf der linken Seite steht derselbe Wert wir auf der rechten Seite. Ohne weitere Modifikation durch Ausdrücke können Variablennutzungen durch Funktionsaufrufe ersetzt werden. i() könnte auch so aussehen:

int i() {
  return c_nachbereitung(s(c_vorbereitung()));
}

Die Variablen in i() sind nur eine Lesehilfe oder könnten bei mehrfacher Nutzung der Performance dienen.

[2] Ich weiß, das soll nicht sein. Und wenn man das Single Responsibility Principle (SRP) beachtet, dann werden einzelne Operationen auch nicht so groß.

Das stimmt. Aber erstens: Beachten Sie das SRP konsequent? Und zweitens: Sehen Sie, was kleine Operationen für eine Folge haben? Sie lassen die Operationenhierarchie weiter wachsen. Die einzelnen Operationen sind zwar kleiner, doch dafür ist die Hierarchie breiter und tiefer.

[3] Wenn Sie bei solcher Übung auf Schwierigkeiten stoßen, Fallunterscheidungen in Operationen auszulagern, ohne die funktional abhängig zu machen, dann ist das selbstverständlich. Aber vertrauen Sie mir: auch das ist ein lösbares technisches Problem.

An dieser Stelle will ich nicht näher darauf eingehen. Doch als Stichwort sei Continuation Passing Style (CPS) genannt.

Mittwoch, 24. April 2013

Raus aus der funktionalen Abhängigkeit

Wenn funktionale Abhängigkeiten so problematisch sind, wie können wir sie denn vermeiden? Ich denke, die Antwort ist ganz einfach:

Wir sollten einfach keine funktionalen Abhängigkeiten mehr eingehen.

Funktion c() wie Client sollte sich nicht mehr an eine Funktion s() wie Service binden. Denn wenn sie das tut, wenn sie nicht mehr delegiert, dann kann sie sich endlich selbstbestimmt auf ihre einzige Domänenverantwortlichkeit im Sinne des Single Responsibility Principle (SRP) konzentrieren.

Technisch bieten Funktionen dafür die besten Voraussetzungen. Die Funktion

int Add(int a, int b) { return a+b; }

macht ja schon nur ihr Ding. Sie definiert sich komplett selbst. Ihre syntaktische Spezifikation besteht nur aus einer Signatur:

Func<int,int,int>

Die könnte man mit einer Beschreibung der gewünschten Semantik ("Die Funktion soll zwei Zahlen addieren.") zur Implementation in ein fernes Land geben. Die Entwickler dort müssten nichts weiter berücksichtigen.

Anders ist das bei einer Funktion wie dieser:

string ReportGenerieren(string dateiname) {
  string report = "...";
  ...
  int[] daten = LadeDaten(dateiname);
  foreach(int d in daten) {
    ...
    report += ...;
    ...
  }
  ...
  return report;
}

Deren Spezifikation besteht nicht nur aus:
  • Func<string,string>
  • "Funktion, die den Report wie folgt generiert: ..."
sondern auch noch aus der Signatur der Funktion zum Laden der Daten und deren Semantik:
  • Func<string,int[]>
  • "Funktion, die die soundso Daten in folgender Weise als int-Array liefert: ..."
Entwickler in einem fernen Land müssten nun diese Dienstleistungsfunktion entweder ebenfalls entwickeln oder gestellt bekommen oder zumindest als Attrappe bauen. Damit ist die Funktion ReportGenerieren() nicht mehr selbstbestimmt.

Allgemein ausgedrückt: Bei funktionaler Abhängigkeit besteht die Spezifikation einer Funktion aus 1 + n Signaturen (mit zugehörigen Semantiken). 1 Signatur für die eigentlich zu betrachtende Funktion und n Signaturen für Funktionen, von denen sie funktional abhängig ist.

Das ist doch kein Zustand, finde ich. So kann es nicht weitergehen, wenn wir zu besser evolvierbarer Software kommen wollen.

Aufbrechen funktionaler Abhängigkeiten

Also, wie kommen wir raus aus funktionaler Abhängigkeit? Wir müssen zunächst genauer hinschauen. Funktionale Abhängigkeit besteht aus drei Phasen:
  1. Vorbereitung
  2. Aufruf
  3. Nachbereitung

int c(int a) {
  // vorbereitung
  int x = ... a ...;
  // aufruf
  int y = s(x);
  // nachbereitung
  int z = ... y ...;
  return ... z ...;
}

Jede abhängige Funktion kann man mindestens einmal in diese Phasen zerlegen. Bei mehreren funktionalen Abhängigkeiten können sich Phasen natürlich auch überlagern. Dann mag eine Nachbereitung die Vorbereitung eines weiteren Aufrufs sein.

Sind diese Phasen aber erkannt, kann man jede in eine eigene Funktion verpacken:

int c_vorbereitung(int a) {
  return ... a ...;
}

int c_nachbereitung(int y) {
  int z = ... y ...;
  return ... z ...;
}

Die bisherige abhängige Funktion wird dann zu einer Folge von unabhängigen Funktionen. Die können so zum bisherigen Ganzen zusammengeschaltet werden:

return c_nachbereitung(s(c_vorbereitung(42)));

Das ist jedoch schlecht lesbar, wenn auch vielleicht schön knapp formuliert. Besser finde ich es daher wie folgt:

int x = c_vorbereitung(42);
int y = s(x);
return c_nachbereitung(y);

Oder moderner könnte es in F# auch so aussehen:

42 |> c_vorbereitung |> s |> c_nachbereitung

Da ist doch sonnenklar, wie die Verantwortlichkeit, für die das ursprüngliche c() stand, erfüllt wird, oder? Das kann man lesen wie einen Text: von links nach rechts und von oben nach unten.

Bitte lassen Sie es sich auf der Zunge zergehen: Keine der Funktionen hat nun noch eine funktionale Abhängigkeit mehr. [1]

c_vorbereitung() ruft keine Funktion auf und weiß nichts von s() und c_nachbereitung(). Dito für s() und c_nachbereitung(). Keine Funktion weiß von einer anderen. Das nenne ich "gegenseitige Nichtbeachtung" (engl. mutual oblivion). Und wenn man sich so bemüht zu codieren, dann folgt man dem Principle of Mutual Oblivion (PoMO).

Funktionen sind ein Mittel, um Funktionseinheiten so zu beschreiben, dass sich nicht um Vorher oder Nachher scheren. Es gibt aber auch andere Mittel. Methoden können mit Continuations versehen werden oder Klassen können mit Events ausgestattet werden.

Egal wie das Mittel aber aussieht, die Funktionseinheit beschreibt immer nur sich selbst. Ihr Interface/ihre Signatur ist rein selbstbestimmt. Bei Funktionen fällt das gar nicht so sehr auf. Deren Ergebnisse sind sozusagen anonym. Anders sieht das aus, wenn Continuations ins Spiel kommen:

void c_vorbereitung(int a, Func<int,int> ???) {
  ???(... a ...);
}

Wie sollte die Continuation ??? heißen? Sollte sie vielleicht s genannt werden? Nein! Dann würde c_vorbereitung() wieder eine semantische Abhängigkeit eingehen. Die Prozedur würde eine bestimmte Dienstleistung erwarten, die ihr übergeben wird.

??? muss vielmehr rein auf die Prozedur bezogen sein. Die Continuation könnte neutral result oder continueWith betitelt sein. Oder sie könnte sich auf den produzierten Wert beziehen:

void c_vorbereitung(int a, Func<int,int> on_x) {
  on_x(... a ...);
}

Einen Vorteil hat eine Continuation gegenüber einem Funktionsergebnis sogar: damit können beliebig viele Ergebnisse zurückgeliefert werden, ohne eine Datenstruktur aufbauen zu müssen.

Vorteile der Vermeidung funktionaler Abhängigkeiten

Inwiefern dient das PoMO aber nun der Vermeidung der durch funktionale Abhängigkeit aufgeworfenen Probleme?

ad Problem #1: Es gibt keine Abhängigkeit, also muss auch keine Bereitstellung erfolgen. Die Frage nach statischer oder dynamischer Abhängigkeit stellt sich nicht einmal. Um c_vorbereitung() oder c_nachbereitung() zu testen, sind keine Attrappen nötig. Von Inversion of Control (IoC) oder Dependency Injection (DI) ganz zu schweigen.

ad Problem #2: Es gibt keine Abhängigkeit, also gibt es auch keine topologische Kopplung. Ob s() auf dem einen oder auf dem anderen Interface definiert ist, interessiert c_vorbereitung() und c_nachbereitung() nicht.

ad Problem #3: Das Wachstum von Funktionen wird ganz natürlich begrenzt. Wann immer eine Funktion eine Dienstleistung benötigt, muss sie beendet werden. Sie bereitet sozusagen nur vor. Für die Nachbereitung ist eine weitere Funktion zuständig. Überlegen Sie, wie lang Funktionen dann noch werden, wenn sie nur noch von einer Delegation bis zur nächsten reichen dürfen. Ich würde sagen, dass es kaum mehr als 10-20 Zeilen sein können, die Sie guten Herzens ohne Verletzung eines anderen Prinzips anhäufen können.

ad Problem #4: Ohne funktionale Abhängigkeit gibt es zumindest keine direkte syntaktische Abhängigkeit. Wenn sich die Form des Output von s() ändert, muss c_nachbereitung() nicht zwangsläufig nachgeführt werden. Es könnte auch eine Transformation zwischen s() und c_nachbereitung() eingeschoben werden:

...
Tuple t = s(x);
int y1 = t.Item1;
return c_nachbereitung(y1);

Das mag nicht immer möglich sein, doch zumindest sinkt das Risiko, dass sich Änderungen an s() ausbreiten.

Dito für eine Änderung der Form der Parameter von s(). Auch hier könnte eine Transformation zwischen c_vorbereitung() und s() die Veränderung abpuffern.

ad Problem #5: Semantische Änderungen, also logische Kopplungen, haben am ehesten weiterhin eine Auswirkung auf Vorbereitung bzw. Nachbereitung. Doch auch hier ist Pufferung möglich, z.B.

Vorher:
int[] sortierteDaten = s(...);
return c_nachbereitung(sortierteDaten);

Nachher:
int[] unsortierteDaten = s(...);
int[] sortierteDaten = unsortierteDaten.Sort();
return c_nachbereitung(sortierteDaten);

Fazit

Die Auflösung funktionaler Abhängigkeit ist immer möglich. Das ist kein technisches Hexenwerk.

Das mag hier und da (zunächst) mühsam sein - doch der Gewinn ist groß. Und wenn man ein bisschen Übung darin hat, dann kann man quasi nicht mehr anders denken. Glauben Sie mir :-) Ich mache das jetzt seit vier Jahren so und es geht inzwischen mühelos.

Sollten funktionale Abhängigkeiten immer aufgelöst werden? Nein. Das wäre Quatsch. Man würde auch in einen infiniten Regress hineinlaufen. Die Frage ist also, wann auflösen und wann nicht? Doch davon ein anderes Mal.

Für heute ist mir nur wichtig, eine Lösung der aufgeworfenen Probleme aufgezeigt zu haben. Die sind aber natürlich gegen andere Werte abzuwägen. Evolvierbarkeit als eine Form von Flexibilität hat ja immer den Gegenspieler Effizienz.

Endnoten

[1] Falls Sie einwenden wollen, dass doch nun irgendwie auf einer höheren Ebene eine Funktion von drei anderen abhängig geworden sei, dann halten Sie den Einwand bitte noch ein bisschen zurück. Sie haben ja Recht. Doch darauf möchte ich in einem späteren Blogartikel eingehen.

Nehmen Sie für heute einfach hin, dass das, was c() mit seiner funktionalen Abhängigkeit war, aufgelöst wurde in drei unabhängige Funktionen.

Donnerstag, 21. März 2013

Softwareentwurf als ökonomische Notwendigkeit

Softwareentwurf ist selbstverständlich kein neues Thema. Früher war Softwareentwurf als der Codierung vorgelagerte Tätigkeit sogar ein zentraler Punkt jeder Informatikerausbildung. Das war dem Mangel an Prozessorkapazität und Speicher geschuldet. Die Turnaround-Zeiten bei der Codierung waren so lang, dass man sich besser gut überlegte, was man schreibt und dann der Übersetzung übergibt und dann laufen lässt. Das Motto lautete geradezu Think before coding. Fehler waren teuer. Ausdrücklicher Softwareentwurf war also ein ökonomisches Gebot.

Ab der zweiten Hälfte der 1980er begannen sich die Verhältnisse jedoch umzukehren, glaube ich. Der Preis für Fehler beim Codieren fiel plötzlich stark. Zuerst wurde die Hardware besser und verfügbarer (Stichwort PC), dann wurden die Codierungswerkzeuge besser (Stichwort RAD).

Der Engpass „Werkzeug“ wurde bald so weit, dass er durch einen anderen abgelöst wurde. Code war nicht länger das Problem, sondern Codierer. Es kam einfach nicht soviel Funktionalität auf die Straße, wie Kunden sich das wünschten. Das hatte mehrere Folgen:

  • Codierer konzentrierten sich mehr und mehr aufs Codieren. Der Softwareentwurf fiel aus der Gnade, da er scheinbar wertvolle Codierkapazität raubte. Man fühlte sich von der Knechtschaft durch die Maschine befreit und schüttelte daher gern damit bisher verbundene Notwendigkeiten ab.
  • Die Ausbildung der Codierer wurde „entformalisiert“, so dass sich schneller mehr von ihnen auf den Markt bringen ließen. Softwareentwurf war Ballast im Curriculum.
  • Man sann auf Mittel, sich das Codieren überhaupt zu sparen. „Wiederverwendbarkeit herstellen!“ wurde zum Schlachtruf der Softwareentwicklung.

Während viele Jahrzehnte lang (die 1950er bis 1980er) die Softwareentwicklung sich im Grunde um sich selbst drehen musste aufgrund einer eklatanten Mangelsituation bei der Hardware, verschob sich ab den 1990ern endlich der Fokus auf den Kunden. Und der ist seitdem unersättlich. Der Hunger nach Software ist ungebrochen, nein, sogar im stetigen Wachsen begriffen. Mehr Software und länger lebende Software sind gefragt. Berge an funktionalen und nicht-funktionalen Anforderungen soll die Softwareentwicklung erfüllen. Und zwar ASAP!

Software überhaupt auf die Straße zu bekommen, ist seitdem wichtiger, als alles andere. Nachfrage nicht zu bedienen kommt teurer als fehlerhafte Software. Nur in wenigen Bereichen ist die ökonomische Priorität bisher anders geblieben: da wo es um Menschenleben geht.

Doch dieses Bild verschiebt sich. Der Engpass hat sich wieder verschoben. Codierer sind nicht mehr das Problem, auch wenn weiterhin Softwareentwickler fehlen. Die grundsätzliche Kapazität von Codierern ist heute durch Hardware, Softwareinfrastruktur, Tools, Bibliotheken sowie Entwicklungs- und laufzeitplattformen so hoch... dass sie sich selbst durch ihre Produktionsweise behindern.

Der Engpass ist von Ressourcen – zuerst Hardware, dann Softwarewerkzeuge und Menschen – zu einem Grundsatz gewandert. Die Theory of Constraints nennt das einen Policy Constraint. Der lautet salopp formuliert: „Wir hauen den Code raus so schnell die Finger tippen!“

Dieser Grundsatz limitiert heute den Durchsatz der Softwareentwicklung. Die kommt nicht ins Stocken, weil sie unter Hardwaremängeln leidet oder softwaretechnisch irgendetwas nicht umsetzbar wäre. Nein, sie stockt, weil ihr Grundsatz einer überholten Ökonomie angehört.

Der Preis dafür, Nachfrage nicht zu bedienen, ist nämlich gesunken. Oder genauer: Der Preis dafür, Nachfrage heute nicht zu bedienen, ist gesunken. Er ist gesunken gegenüber dem Preis für die Auslieferung fehlerhafter Software. Vor allem wird er aber immer kleiner im Vergleich zu dem, was es kostet, morgen Nachfrage nicht mehr bedienen zu können.

Modern ausgedrückt ist der langjährige Grundsatz nicht nachhaltig.

Das war früher kein Problem. Genauso wie es früher kein Problem war, wenn eine Handvoll Menschen in einem großen Wald Bäume für den Hüttenbau und das Herdfeuer abholzte.

Für große, hungrige Zivilisationen funktioniert derselbe Umgang mit dem Wald jedoch nicht.

Wenn bei naiver Nutzung der Ressourcenbedarf über der „Ressourcenauffrischung“ liegt, dann ist das kein nachhaltiger Umgang mit Ressourcen. Dann kann man heute daraus Nutzen ziehen – aber morgen nicht mehr.

So ist das auch bei „naiver“ Softwareentwicklung. Die funktioniert solange auf grüner Wiese codiert wird. Das war viele Jahrzehnte der Fall. Der Markt war grün, die Software war grün. Es gab keine Altlasten. Und der Hunger nach Funktionalität war so groß, dass alles andere nur wenig zählte. Da konnte man überall seine „braunen Flecken“ hinterlassen. (Von drastischerem Vokabular sehe ich hier einmal ab.)

Doch das hat sich in den letzten 10-15 Jahren verändert. Die Kunden werden anspruchsvoller, sie tolerieren immer weniger Bugs. Vor allem aber muss heute Software schneller und enger am Kunden entwickelt werden. Die Budgets sind gesunken. Und Software ist umfangreicher denn je, muss also länger leben. Auch das ist eine Budgetfrage.

Die grüne Wiese ist bis zum Horizont ein braunes Feld geworden.

Deshalb ist nachhaltige Softwareentwicklung das Gebot der Stunde. Deshalb ist der bisherige Grundsatz der neue Engpass als Policy Constraint.

Daraus folgt für mich aber nicht nur, dass mehr Qualitätssicherung und automatisierte Tests und agile Vorgehensmodelle sein müssen. Es folgt auch, dass Softwareentwurf wieder einen festen Platz in der Softwareentwicklung bekommen muss. Das scheint mir eine ökonomische Notwendigkeit im Rahmen der historischen Bewegungen unserer Disziplin. Denn ausdrücklicher Softwareentwurf macht sich Gedanken über das Big Picture und das Morgen. Wer ständig über dem Code hängt und dort lokale Optimierung betreibt, der fliegt am Ziel Nachhaltigkeit vorbei.

Auszug aus dem Buch “Flow-Design – Pragmatisch agiler Softwareentwurf”, an dem ich nun endlich doch arbeite. Jeden Tag 4 Stunden schreiben… Puh… Demnächst mehr dazu hier im Blog.

PS: Gerade stolpere ich über eine Aussage von Dijkstra:

“[The major cause of the software crisis is] that the machines have become several orders of magnitude more powerful! To put it quite bluntly: as long as there were no machines, programming was no problem at all; when we had a few weak computers, programming became a mild problem, and now we have gigantic computers, programming has become an equally gigantic problem.”

Das entspricht ganz meinem Gefühl – auch wenn das, was er 1972 mit “gigantic computers” gemeint hat, heute lächerlich wirkt.

Freitag, 1. Februar 2013

Softwareentwurf in der hohlen Hand

Gerade komme ich wieder von drei Trainingstagen zum Thema agiler Softwareentwurf. Ein inhouse Training für ehemalige C bzw. C++ Entwickler, die sich noch an .NET gewöhnen müssen. Aber es hat vielleicht gerade deshalb besonders Spaß gemacht, weil sie noch nicht so “objektorientiert verdorben” sind. Ihnen steckt vor allem die C-Praxis in den Knochen, die sie anscheinend offen für den Softwareentwurf mit Softwareuniversum und Flow-Design gemacht hat. Denn wenn die prozedurale Programmierung in einem Recht hatte, dann darin, dass es zuerst und vor allem um Funktionalität und nicht um Daten geht.

Aber von dem positiven Feedback, das Stefan Lieser und ich dort bekommen haben, wollte ich gar nicht sprechen. Die gute Stimmung bei den Trainingsteilnehmern hat vielmehr mich selbst nochmal unseren Ansatz frisch erleben lassen.

Abends saß ich nämlich im Hotel bei einem gemütlichen Kaminfeuerchen und habe an einer Anwendungsidee herumgebastelt. Eine Web-Anwendung, die helfen soll, eine Klippe des Kennenlernens über online Partnerbörsen zu umschiffen. Wie das gehen soll wird die dotnetpro demnächst berichten ;-) Technologisch mit von der Partie werden jedenfalls AppHarbor, Nancy und iron.io sein.

Da saß ich also und habe darüber nachgedacht, wie die Idee in Software funktionieren könnte. Welche Seiten sollte so ein Website haben? Wie sollten die Seitenübergänge sein? Was sollte dabei passieren? Welche Klassen/Komponenten sollte es geben? Wie würden Feature ineinander greifen?

Im Kopf kann ich so etwas bewegen – aber nur bis zu einer gewissen Grenze. Dann muss ich einen Dump machen. Irgendwie muss ich meine Gedanken und Ergebnisse manifestieren. Visual Studio hatte ich vor dem Kamin aber nicht zur Hand. Warum auch? Ich brauche keinen Code, um Software zu entwerfen.

So hat mir denn ein kleiner Notizblock des Hotels geholfen. Der war nur handtellergroß mit schmalen Blättern. Aber er hat mir gereicht wie diese Bilder zeigen:

image

image

Wenn Sie nicht wissen, was die Symbole bedeuten und meine Schrift nicht lesen können, macht das nichts. Ich will Ihnen ja nicht erklären, was die Anwendung tut und wie ich gedenke, das zu realisieren.

An dieser Stelle soll nur rüberkommen, dass ich mit zwei kleinen Zetteln die Software entwerfen konnte. Ich weiß nun, welche Klassen ist brauche. Ich weiß, welche Methoden die haben. Ich weiß, wie alles zusammenhängt und die Funktionalität herstellt. Ich weiß, welche Daten fließen. Ich weiß, wie ich APIs kapseln muss. Ich weiß sogar schon, wie ein guter Teil des Codes aussehen muss.

Den Entwurf der beiden Bilder kann ich “so runtercodieren”. Da ist alles drin, was ich brauche. Nur das Layout der Seiten fehlt. Aber das gehört ja auch nicht zum funktionalen Entwurf. Wichtig ist dafür nur, welche Interaktionen der Benutzer mit Dialogen hat. Und die stecken in dem Modell drin.

Durch das Training geöffnet habe ich frisch erlebt, wie effektiv Softwareentwurf mit Papier und Stift sein kann – wenn man eine geeignete Methode hat ;-) Ich muss nicht darauf warten, dass Softwarestrukturen durch Tests wachsen. Ich brauche keine IDE, um bei einer Anwendung voran zu kommen.

Klar, am Ende muss implementiert werden. Bubbles don´t crash. Ich weiß nicht, ob mein Entwurf 100% korrekt/ausreichend ist. Aber ich bin ein gutes Stück voran gekommen. Ich habe mit “systematischer Kritzelei” das Codieren solide vorbereitet. Wenn ich dann Details mit TDD austreiben will, weiß ich, wo ich ansetzen kann.

Und all das steckt in zwei Blättchen mit Flow-Designs. Das fand selbst ich, der ich täglich mit Flow-Design arbeite, irgendwie bemerkenswert cool.

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.