Follow my new blog

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.

Donnerstag, 25. April 2013

Selbstorganisation persönlich definiert

Was hat Agilität mit Selbstorganisation zu tun? Diese Frage stellte sich gerade in einem kleinen Twittergespräch mit Michel Löhr aka @1ohr.

Mein Standpunkt: Agilität und Selbstorganisation sind orthogonal.

Selbstorganisation hat etwas mit der Führung einer Organisation zu tun. Agilität hat etwas mit Art der Herstellung von etwas zu tun.

Agil ist die Herstellung für mich, sobald sie inkrementell ist, der Hersteller darauf bedacht ist, zu lernen (konkret und auf der Meta-Ebene), und die Kommunikation zwischen den Beteiligten eng ist. Das halte ich für eine pragmatische Definition von Agilität. Nicht zu eng, dass nur die 100%ige Einhaltung eines Vorgehensmodells adelt, aber auch nicht zu weit, als dass sie jeder als agil nennen könnte. Ausführlicher habe ich das in einem früheren Artikel erklärt.

Soweit die Agilität. Aber was ist mit der Selbstorganisation? Steckt die notwendig in der Agilität drin? Ich denke, nein. Weder inkrementelle Lieferung, noch das Lernen und auch nicht eine engere Kommunikation setzen Selbstorganisation voraus. Das alles mag mit Selbstorganisation noch besser gehen, doch sie ist für mich nicht notwendig und schon gar nicht hinreichend für Agilität [1].

Das sage ich natürlich mit einer Vorstellung von Selbstorganisation im Hinterkopf.

Soziale Systeme

Der Begriff "Selbstorganisation" beschwört natürlich einige Bilder herauf. Für manche ist dann die Anarchie nicht mehr fern. "Wenn jeder das machen kann, was er will, wo kommen wir denn dann hin?" Aber ist das Selbstorganisation, wenn jeder machen kann, was er will?

Natürlich nicht. Denn wenn jeder machen kann, was er will, dann gibt es keine Organisation im Sinne eines Zusammenschlusses von mehreren Menschen. Selbstorganisation ist nur ein Thema für soziale Systeme. Selbstorganisation setzt also voraus, dass mehrere Menschen zusammenkommen. Der Begriff bezieht sich immer auf einen Menschenansammlung.

Aber auch das ist noch zuwenig. Denn eine Menschenansammlung gibt es an jeder Bushaltestelle, wo Leute zusammen stehen. Die gehören aber nicht zusammen. Da verfolgt jeder seine eigenen Ziele, auch wenn sie im Sinne der Busbenutzung und des Wunsches, verlässlich und sicher befördert zu werden, eine Gemeinschaft sind.

Selbstorganisation ist nur ein Thema bei zusammengehörigen Menschen. Es braucht ein Ziel, dem die Menschen aktiv zustreben. Dadurch wird aus einer Gemeinschaft erst eine Gruppe.

Eine Gemeinschaft wird zwar durch ein gemeinsames Interesse charakterisiert, doch das wird von der Gemeinschaft noch nicht gemeinschaftlich aktiv verfolgt. Die Gemeinschaft überlässt das jemandem anderen (z.B. Busfahrer, Priester) oder jeder verfolgt selbst das Interesse nach seinem Gusto (z.B. Briefmarkensammler, Glaubensanhänger).

Eine Gruppe hingegen hat einen außen liegenden Zweck. Auf den hin wird sie geführt. Ebenfalls von außen. Deshalb ist die kleinste Einheit bei der Bundeswehr eine Gruppe mit einem Gruppenführer.

Aber eine Gruppe ist noch nicht selbstorganisiert. Sie ist geradezu das Gegenteil. Sie wird von jemandem anderen geführt, d.h. organisiert. Wer was wann und vielleicht auch wie tun soll, wird angesagt. Das ist auf einer Baustelle dasselbe, wenn der Polier die Maurer anweist. Das ist auch im Operationssaal so, wenn der Operateur "Schwester, Tupfer, bitte." sagt.

Gruppen entstehen immer dann, wenn Menschen koordiniert werden. Wenn einer die Richtung weist, die sie laufen sollen.

Das ist nicht per se gut oder schlecht. Das ist einfach nur ein Mittel, um ein Ergebnis mit mehreren Menschen zu erreichen.

Sind solche Gruppen denn nicht aber Teams?

Im landläufigen Sprachgebrauch ist das so. Aber für mich braucht es mehr, um aus einer Gruppe ein Team zu machen.

Ein Team ist eine Gruppe, bei das äußere Ziel zu einem gemeinschaftlichen inneren Ziel geworden ist. Und bei dem das Ziel in Selbstorganisation angestrebt wird.

Teams sind in Bezug auf ein Ergebnis intrinsisch motiviert. Das macht es ja so schwer, echte Teams herzustellen. Da ist nämlich Führungskunst auf höherer Ebene gefragt.

Und Teams entscheiden anschließend zumindest alles in Bezug auf das gemeinschaftliche Ergebnis Relevante selbst. Sie entscheiden über Mittel, Wege, Zeit - in einem gewissen vorgegebenen Rahmen. Teams sind insofern wie ein Marschflugkörper: Sie werden mit einer Aufgabe und einem Sack voll Ressourcen abgeschossen - und bewegen sich danach selbstständig ins Ziel. Dass sie dabei auf ihre Umwelt reagieren, tut dem keinen Abbruch. Denn sie entscheiden über ihre Reaktion.

Selbstorganisation

Jetzt habe ich gesagt, wo Selbstorganisation angesiedelt werden kann. Aber was ist denn nun diese Selbstorganisation?

Die Definition bei Wikipedia finde ich interessant - aber für den Hausgebrauch viel zu weitschweifig und abstrakt. Für mich ist Selbstorganisation viel einfacher immer dann vorhanden, wenn eine Gruppe eine Entscheidung über den einzuschlagenden Weg zu einem Ergebnis selbst treffen darf.
  • Soll mit Java oder .NET entwickelt werden? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf die Entwicklungsplattform selbstorganisiert.
  • Soll agil vorgegangen werden? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf das Vorgehensmodell selbstorganisiert.
  • Soll Peter der ScrumMaster sein und Klaus die CI einrichten? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf die Rollenbesetzung selbstorganisiert?
  • Soll das Daily Standup von 10 Minuten auf 15 Minuten ausgedehnt werden? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf das Lernen im Rahmen ihres Vorgehensmodells selbstorganisiert.
  • Soll jeden Dienstag von 15-19h eine gemeinschaftliche "Lernzeit" in der CCD School stattfinden, um die Weiterentwicklung in Bezug auf Clean Code und andere Aspekte nachhaltig zu betreiben? Wenn eine Entwicklergruppe darüber selbst entscheiden darf, dann ist sie in Bezug auf ihre Zeiteinteilung und das Budget selbstorganisiert.
Sie sehen: Selbstorganisation ist binär. Sie ist da oder nicht. In Bezug auf einen Aspekt der Arbeit. Insofern ist Selbstorganisation auch immer irgendwie vorhanden. Einfach nur deshalb, weil sie Gruppenführer nicht um jeden Mist kümmern können und wollen. Immer wird der geführten Gruppe irgendetwas zur eigenen Entscheidung überlassen.

Gerät die Führung jedoch unter Druck, reagiert sie oft mit Einschränkung der Selbstorganisation. Dann werden Entscheidungsfreiheiten beschnitten. Es wird enger geführt. Micro-Management ist das Gegenteil von Selbstorganisation.

Von "der Selbstorganisation" wird allerdings erst gesprochen, wenn die de facto Selbstorganisation einen gewissen Schwellenwert überschritten hat. Solange eine Gruppe nur über Triviales selbst entscheiden kann, wird sie gewöhnlich und zurecht nicht als selbstorganisiert bezeichnet. Dann ist sie kein Team.

Ich denke, von erwähnenswerter (und dadurch auch kontroverser) Selbstorganisation kann erst gesprochen werden, wenn eine Gruppe sich selbst führt und nicht nur koordiniert. Das bedeutet, sie hat die Hoheit über Grundsatzentscheidungen jenseits der streng fachlichen. Dazu gehören für mich:
  • Arbeitsinhalt
  • Arbeitsort
  • Arbeitszeit
  • Arbeitsmittel
  • Rollenverteilung
Nein, durch die Freiheit zur Entscheidung in all diesen Belangen, entsteht keine Anarchie. Das hieße, Menschen als grob naiv und fahrlässig und desinteressiert anzusehen.

Außerdem bedeutet Entscheidungsfreiheit ja nicht Unbegrenztheit. Selbst ein Kurierfahrer fühlt sich auf der Straße ja frei, obwohl er erstens einen Auftrag hat und zweitens die StVO einhalten muss und drittens anderen Verkehrsteilnehmern mutwillig keinen Schaden zufügen will.

Selbstorganisation ist möglich in Grenzen. Nein, ich würde sogar sagen, sie braucht Grenzen. Die setzen nämlich Kreativität frei. Das ist wie beim Künstler, der seine Meisterschaft in den Grenzen von Motiv und Mittelwahl beweist.

Selbstverständliche Grenzen sind das gemeinschaftliche Ziel und die Professionalität der Teammitglieder [3].

Aber solange ein Unternehmen noch an Budgets glaubt, darf es einem Team auch ein Budget vorgeben. Dessen Einhaltung ist dann ein Ziel wie die Auslieferung von Software, die bestimmte Anforderungen erfüllt.

Genauso muss sich ein Team verantworten. Es arbeitet nicht im luftleeren Raum.

Doch die Grenzen sollten locker sein. Und die, die sie vorgeben, sollten sich ansonsten im Hintergrund halten. Sie sollten sich für Ergebnisse interessieren und nicht für den Weg, den das Team in Selbstorganisation wählt.

Fazit

Ob Sie in einem Team arbeiten, können Sie nun selbst prüfen. Hat ihre Entwicklergruppe Entscheidungsfreiheit in den angeführten Belangen? Dann arbeiten Sie leider noch nicht wirklich in einem Team. Da helfen auch aller guter Wille und Grillabende nichts.

Und wie steht es mit der Agilität und Selbstorganisation? Ich hoffe, Sie stimmen mit mir überein, dass Agilität "die Selbstorganisation" nicht voraussetzt. Agil können auch Gruppen arbeiten. Dafür braucht es nicht zwingend Teams [2]. Aber mit Teams geht Agilität wie vieles andere auch in der Softwareentwicklung natürlich besser.

Agilität mag ohne Selbstorganisation nicht wünschenswert sein, aber sie ist möglich.

Endnoten

[1] Wenn ich hier "Selbstorganisation" so pauschal gebrauche, dann adressiere ich damit auch eine ebenso pauschale (wie diffuse) Vorstellung davon. Umfassende Selbstorganisation halte ich für nicht nötig für die Agilität.

Andererseits lässt sich Selbstorganisation ja aber nicht vermeiden, wie Sie bei der weiteren Lektüre feststellen werden. Insofern ist Selbstorganisation selbstverständlich immer Teil von Agilität oder gar notwendig. Das halte ich jedoch für trivial.

[2] Ich denke, das beweist auch die Praxis, in der sich viele Projekte agil nennen, aber nur von Gruppen durchgeführt werden. Selbstorganisation, ich meine echte Selbstorganisation, ist einfach nicht weit verbreitet. Wäre doch schade, wenn sich deshalb die Agilität nicht ausbreiten könnte.

[3] Deshalb ist es so wichtig, beim Übergang zu mehr Selbstorganisation an der Professionalität zu arbeiten. Die ist nämlich durch die lange Gängelung unter einer Gruppenführung atrophiert. Nicht jeder fühlt sich deshalb auch gleich wohl, wenn er mehr Entscheidungsrecht und -pflicht bekommt.

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.

Sonntag, 21. April 2013

Warnung vor der funktionalen Abhängigkeit

Immer noch bin ich dem Gefühl auf der Spur, warum funktionale Abhängigkeiten "böse" sind. Sie sind so natürlich, so naheliegend - und doch beschleicht mich immer wieder Unwohlsein. Ich versuche schon lange, sie zu vermeiden. Deshalb betreibe ich Flow-Design. Doch einem Gefühl allein will ich mich da eigentlich nicht hingeben. Es mag als Auslöser für eine Veränderung alter Gewohnheiten sehr nützlich sein, nur ist es schwer, rein aufgrund eines Gefühls diese Veränderung weiter zu tragen. Deshalb will ich noch Mal genauer hinschauen. Gibt es eine rationale Erklärung für mein Gefühl? Ich habe dazu schon Anläufe hier im Blog genommen. Doch jetzt will ich es mehr auf den Punkt bringen.

Funktionale Abhängigkeit

Für mich besteht eine funktionale Abhängigkeit, wenn...
  1. eine Funktion c() wie Client eine Funktion s() wie Service aufruft und
  2. c() selbst die Parameter für s() durch Berechnungen vorbereitet und
  3. c() selbst das Ergebnis von s() selbst in Berechnungen weiterverarbeitet.
Wenn c() in dieser Weise s() benutzt, dann braucht c() s().

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  return ... z ...;
}

c() ist funktional abhängig, weil c() Logik enthält, die auf s() abgestimmt ist. Allemal sind das die Berechnungen, die auf den Aufruf von s() folgen.

Wie gesagt, so zu arbeiten, ist ja total normal. Am Ende können und wollen wir auch nicht ohne. Nur ist mein Gefühl, dass wir zu oft in diesem Stil arbeiten. Zu gedankenlos gehen wir die Probleme ein, die er mit sich bringt. Das Resultat: Code, der sich alsbald nur noch schwer verändern lässt. So werden Brownfields produziert.

Problem #1: Bereitstellungsaufwand

Das erste Problem mit solcher funktionalen Abhängigkeit scheint ein Feature - ist aber ein "Bug". Es besteht darin, dass c() überhaupt auf s() zugreifen muss. Das kann in statischer Weise geschehen: c() bzw. die Klasse, zu der c() gehört, kennt die Klasse, zu der s() gehört. Also wird bei Bedarf eine Instanz der unabhängigen Serviceklasse erzeugt:

int c(int a) {
  U u = new U();
  ...
  int y = u.s(x);
  ...
}

Ok, dass das "böse" ist, wissen wir inzwischen alle. Bestimmt tun Sie das schon lange nicht mehr, sondern folgen dem Prinzip Inversion of Control (IoC) und benutzen Dependency Injection (DI).

class A {
  IU _u;
  public A(IU u) { _u=u; }

  public int c(int a) {
    ...
    int y = _u.s(x);
    ...
  }
}

Die abhängige Klasse A von c() kennt nur eine Abstraktion der unabhängigen Klasse U von s(), z.B. in Form des Interface IU. Jedes A-Objekt bekommt dann ein Objekt irgendeiner Klasse injiziert, die IU implementiert. Das kann eine Instanz von U sein, aber auch eine Attrappe.

Aus der statischen Abhängigkeit ist somit eine dynamische geworden. Die Kopplung zwischen c() und s() ist gesunken. c() ist besser testbar. Wunderbar. Und dennoch...

Das Problem besteht für mich darin, dass wir uns überhaupt um so etwas Gedanken machen müssen. IoC/DI sind nur Symptomkuren. Ohne funktionale Abhängigkeit würde sich die Frage nach statischer oder dynamischer Kopplung nicht einmal stellen.

Wer das Single Responsibility Principle (SRP) ernst nimmt, der muss doch sehen, dass A hier zwei Verantwortlichkeiten hat. Da ist zum einen die Hauptverantwortlichkeit von A, seine Domäne, in der c() eine Funktionalität bereitstellt.

Darüber hinaus leistet A aber auch noch auf eine technischen Ebene etwas. A stellt s() bereit. Auf die eine oder andere Weise. Dass c() überhaupt Zugriff auf s() haben kann, ist eine eigenständige Leistung, die nicht zur "Geschäfts"domäne von A gehört.

Funktionale Abhängigkeit vermischt zwei Aspekte. Das wird noch deutlicher beim zweiten Problem, das ich sehe:

Problem #2: Topologische Kopplung

Die Vermischung der Domäne mit dem "Bereitstellungsaspekt" mag für Sie quasi schon unsichtbar sein. Sie ist total normal und scheint unvermeidbar. Aber das ist noch gar nichts. Es wird noch subtiler.

Nehmen wir an, c() ist funktional abhängig von zwei Funktionen, s1() und s2().

class A {
  IU _u;
  public A(IU u) { _u=u; }

  public int c(int a) {
    ...
    int y1 = _u.s1(x);
    ...
    int y2 = _u.s2(z);
    ...
  }
}

Was passiert, wenn Sie in einem Anfall von SOLIDer Programmierung das Interface Segregation Principle (ISP) anwenden und sich entscheiden, s1() und s2() auf verschiedene Interfaces zu legen?

Sie müssen A ändern. Obwohl sich an der Domäne von A nichts geändert hat. Das ist ein klarer Widerspruch zum SRP, das "once [business] reason to change" zur Einhaltung fordert.

Die gut gemeinte Refaktorisierung nach dem ISP zieht so einiges nach sich für A:

class A {
  IU1 _u1;
  IU2 _u2;
  public A(IU1 u1, IU2 u2) { _u1=u1; _u2=u2; }

  public int c(int a) {
    ...
    int y1 = _u1.s1(x);
    ...
    int y2 = _u2.s2(z);
    ...
  }
}

A muss den Änderungen an der Verteilung der Servicefunktionen auf Interfaces nachgeführt werden, weil A nicht nur von s1() und s2() abhängig ist, sondern auch daran gekoppelt ist, wo (!) diese Funktionen definiert sind. A ist an die Topologie ihrer Umgebung gekoppelt.

Auch das scheint alles ganz normal und unvermeidbar. Dafür gibt es Refaktorisierungswerkzeuge, oder?

Könnte man so sehen. Aber ich finde es besser, sich gar nicht erst in den Fuß zu schießen, als die Wunder der modernen Medizin loben zu müssen.

Interessanterweise wird die topologische Kopplung zu einem umso größeren Problem, je sauberer Sie mit funktionalen Abhängigkeiten programmieren wollen. Denn desto mehr Klassen bekommen Sie (SRP) und desto mehr Interfaces auf diesen Klassen wird es geben (ISP). Der Anteil an "Bereitstellungsinfrastruktur" in den abhängigen Klassen wird immer größer.

Problem #3: Grenzenloses Wachstum

Umfangreiche Funktionen kennen Sie bestimmt auch. Ich habe schon welche mit 10.000 Lines of Code (LOC) gesehen. Sie kennen auch die Empfehlungen, den Funktionsumfang zu begrenzen. Manche sagen, Funktionen sollten nur 10 oder 20 oder 50 LOC enthalten. Andere sagen, sie sollten nicht länger als eine Bildschirmseite sein. Wieder andere beziehen sich nicht auf die LOC, sondern auf die "Komplexität" des Codes in Funktionen (die mit der Zahl der möglichen Ausführungspfade durch ihn zu tun hat).

An guten Ratschlägen mangelt es wahrlich nicht, das uralte Problem der beliebig umfangreichen Funktionen in den Griff zu bekommen. Nützen tun sie aber offensichtlich wenig. Denn bei allem guten Willen wachsen Funktionen auch bei Ihnen wahrscheinlich weiterhin. Sie mögen das Wachstum bewusster verfolgen, es in Code Reviews immer wieder diskutieren, Maßnahmen zur Eindämmung ergreifen (Refaktorisierung)... Doch am Ende ist jede Metrik dehnbar. Ihre besten Absichten können und werden Sie aussetzen, wenn eine andere Kraft nur stark genug ist.

Das ist so. Darüber sollten wir uns nicht grämen. Es bleibt nämlich so. Unser Fleisch ist schwach. Der Nahkampf mit dem Code im Tagesgeschäft ist dreckig.

Trotzdem, nein, deshalb lohnt eine Frage nach der Ursache solchen grenzenlosen Wachstums. Wie kann es dazu überhaupt kommen? Das passiert ja nicht einfach. Wir tun es. Aber warum?

Wie lassen Funktionen grenzenlos wachsen, weil wir es können.

So einfach ist das.

Und das, was es uns möglich macht und motiviert, Funktionen wachsen zu lassen, das ist die funktionale Abhängigkeit.

Ja, so einfach ist das, glaube ich.

Alles beginnt mit etwas Logik in c():

int c(int a) {
  int x = ... a ...;
  return ... x ...;
}

Dann kommt eine funktionale Abhängigkeit dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  return ... x ...;
}

Und dann kommt noch eine dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  int k = t(z);
  return ... k ...;
}

Und dann kommt noch eine dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  int k = t(z);
  int l = ... k ...;
  int m = v(l);
  return ... m ...;
}

Das geht so weiter, weil nach dem Funktionsaufruf vor dem Funktionsaufruf ist.

Die eigentliche Logik von c() kann beliebig fortgeführt werden, weil ja bei Bedarf immer wieder die Berechnung von Teilergebnissen durch Delegation (funktionale Abhängigkeit) eingeschoben werden kann.

Klingt normal. Dafür sind Funktionsaufrufe ja da. Das möchte ich ganz grundsätzlich auch nicht missen.

Aber wir müssen erkennen, dass genau diese Möglichkeit dazu führt, dass abhängige Funktionen grenzenlos wachsen.

Dem können wir dann zwar mit SOLIDem Denken und Refaktorisierungen entgegenwirken - doch das sind eben Maßnahmen, die angewandt werden können oder auch nicht. Ob das SRP noch eingehalten ist, ob eine Methode schon zu lang ist und refaktorisiert werden sollte... das entsteht im Auge des Betrachters. Darüber kann man sich trefflich streiten in einem Code Review. Und im Zweifelsfall lässt man die Finger von der Veränderung laufenden Codes.

Problem #4: Syntaktische Kopplung

Wenn c() die Funktion s() aufruft, dann muss c() wissen, wie genau das zu geschehen hat. c() muss die Form, die Syntax von s() kennen. Wenn s() einen int-Parameter brauch und sein Ergebnis als int zurückliefert, dann muss c() sich darauf einstellen.

Was aber, wenn die Form von s() sich ändert? Parameter könnten zusammengefasst werden, Parameter könnten Typ oder Reihenfolge ändern... Dann muss c() sich dieser Änderung beugen.

c() muss sich mithin also ebenfalls ändern, wenn sich "nur" an der Form von s() etwas ändert. Und das hat mit der Domäne, der single responsibility von c() nicht unbedingt etwas zu tun.

Problem #5: Semantische Kopplung

Wenn c() die Funktion s() braucht, um anschließend mit deren Ergebnissen weiter zu rechnen, dann ist c() von der Semantik von s() abhängig.

Wenn s() z.B. heute eine Liste von sortierten Daten produziert, die c() weiterverarbeitet, dann kann es eine für c() relevante semantische Veränderung sein, dass s() morgen diese Liste nicht mehr sortiert zurückliefert.

Syntax und Semantik sind orthogonal. An der Syntax kann sich etwas verändern ohne Semantikveränderung. Genauso kann sich die Semantik ohne Syntaxveränderung verschieben. In jedem Fall hat c() nicht die Hoheit über seine innere Ausprägung, sondern muss sich s() beugen. Weil s() "in c() eingebettet ist".

Fazit

Das scheinen mir fünf sehr rationale Gründe für mein Unwohlsein in Bezug auf funktionale Abhängigkeiten. Das ist alles nichts Neues, würde ich sagen. Damit leben wir täglich. Und wenn die Probleme uns nicht so explizit bewusst sind, so leiden wir dennoch an ihnen. Sie verursachen uns Schmerzen beim Ändern oder Verständnis von Code.

Jedes einzelne Problem erscheint vielleicht klein. Und die Auswirkungen sind ja auch unterschiedlich nach Domäne von c() und Form und Semantik von s1(), s2() usw. In Summe jedoch machen diese Probleme aus scheinbar simplen funktionalen Abhängigkeiten eine große Last, glaube ich.

Wir werden nicht ohne funktionale Abhängigkeiten auskommen. Ich möchte die Möglichkeit, sie einzugehen, auch nicht abschaffen. Sie sind ein Mittel zur effizienten Lösung von Problemen. Das wollen wir in unserem Programmierhandwerkszeugkasten haben.

Dieses Mittel darf nur kein Selbstzweck sein. Und es darf sich nicht als alternativlos darstellen. Programmierung mit funktionalen Abhängigkeiten gleichzusetzen, würde sie auf ein unverdientes Podest heben.

Mir geht es deshalb um eine Bewusstmachung und eine Begrenzung. Bindungen bewusst eingehen und immer wieder fragen, ob sie noch ihren Zweck erfüllen, ist wichtig. Je größer die potenziellen Probleme durch Bindungen, desto mehr sollte auch geprüft werden, ob es Alternativen gibt. Denn oftmals werden Bindungen enger geschnürt als nötig. Weil man nicht weiß, wie es anders gehen könnte oder weil man die Notwendigkeit größerer Nähe überschätzt (Premature Optimization).

Mit diesen fünf guten Gründen habe ich also jetzt zumindest für mich geklärt, was mein Gefühl ausmacht, dass funktionale Abhängigkeiten "böse" sind. Ich halte sie für ausreichend, um meine Praxis der Minimierung funktionaler Abhängigkeiten fortzuführen. Denn es gibt Möglichkeiten, sie zu vermeiden.

Mittwoch, 3. April 2013

Clean Code Development anders lernen

Ich hatte einen Traum. Den erfülle ich mir nun. Zusammen mit Stefan Lieser eröffne ich die Clean Code Developer School (CCDS):

image

Mein Traum war schon vor Jahren, Softwareentwicklung entspannter und nachhaltiger für die Teilnehmer zu unterrichten. Zunächst hatten Stefan und ich das mit den CCD Camps versucht, deren Trainings bis zu 14 Tage gedauert haben.

Da hat uns dann aber die Realität schnell eingeholt. So lange mögen Unternehmen ihre Mitarbeiter nicht “von der Leine lassen”. Auch die Aufteilung in kleinere Blöcke mit Pausen dazwischen hat die Attraktivität solch soliden, ausführlichen Lernens nicht erhöht.

Die Konsequenz: Inzwischen sind sind runter auf die üblichen 2, 3 oder maximal 5 Tage Trainingseinheiten. Wir versuchen, die Clean Code Developer Inhalte so zu schneiden, dass Teilnehmer in so kurzer Zeit wirklich etwas mitnehmen.

Leider zeigt sich, dass schon 5 Tage am Stück zu lernen, eine starke Belastung für die Aufmerksamkeitsspanne ist. Ab Tag 4 ist deutlich zu spüren, wie die Luft bei den Teilnehmern ausgeht. Der Lernstoff muss einfach mal sacken, statt unter weiterem begraben zu werden.

Doch die übliche Ökonomie von Trainings drängt darauf, soviel wie möglich in kurze Trainings zu stecken.

Darauf habe ich jetzt aber keine Lust mehr. Und Stefan Lieser auch nicht.

Wir sehen einfach, dass oft das Geld für die üblichen Trainings rausgeschmissen ist. Zuviel Lernstoff in zu kurzer Zeit führt dazu, dass am Ende nichts richtig geübt wurde. Aber vor allem fallen die Teilnehmer nach dem Training in ein Loch. Zurück am Arbeitsplatz fehlt ihnen die Zeit und die Anleitung, um den Stoff weiter zu üben und dann auf ihre Situation zu transferieren.

Das ist für uns als engagierte Trainer frustrierend zu sehen.

Deshalb machen wir es jetzt anders.

Einstweilen geben wir unsere üblichen Trainings zum Beispiel im Rahmen der Clean Code Developer Akademie noch nicht auf. Aber zumindest setzen wir einen Fuß in eine neue Richtung mit der Clean Code Developer School.

Dort findet Lernen nämlich nicht in tagelange Kursen ab und an im Jahr statt, sondern jede Woche in Blöcken zu 4 Stunden.

Wir meinen das mit dem “regelmäßig lernen” ernst. Ohne geht es einfach nicht. Nur findet es am Arbeitsplatz nicht statt. Deshalb machen wir mal ein ganz anderes, quasi ein unwiderstehliches Angebot. Wir kommen mit der CCDS den Lerninteressierten im doppelten Sinn so weit entgegen, dass kontinuierliches Lernen machbar wird.

Natürlich sind wir uns bewusst, dass ein solches Angebot schlechter skaliert als übliche Trainings. Ob und wie es sich für uns rechnen wird, müssen wir sehen. Aber egal. Wir starten das Experiment, weil wir das Richtige im Sinne eines nachhaltigen Lernens für die Teilnehmer tun wollen.

Details zum Was, Wie, Wo, Wann finden Sie auf der Seite der Clean Code Developer School. Hier will ich nicht mehr verraten ;-)

Falls Sie in der Nähe eines Schulungsortes der CCDS sein sollten, schauen Sie doch zumindest mal für einen Schnupperunterricht rein.

Wenn ich Sie für diese Idee begeistern konnte, dann folgen Sie der CCDS doch ab jetzt über ihren RSS-Feed oder bei Twitter: @ccd_school.