Follow my new blog

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.

Freitag, 29. März 2013

Ab auf die Walz – Software Craftsmanship ernst genommen

Jetzt will ich es endlich tun. Ich will auf die Walz gehen. Schon länger hatte ich mit dem Gedanken gespielt – doch mehr als der prio walk mit Stefan Lieser von München nach Nürnberg ist bisher nicht draus geworden.

Nun möchte ich des Rest aber nachholen. Gewandert bin ich genug. Jetzt möchte ich mitmachen. An verschiedenen Stationen möchte ich Teamkulturen kennenlernen, indem ich für eine begrenzte Zeit “ganz normal” mitarbeite. Als Trainer und Berater sehe ich zwar viele Teams, doch ist da mein Auftrag ihnen bei Veränderungen zu helfen. Deshalb erfahre ich nicht unbedingt, wie sie heute leben und weben. Das möchte ich ändern, indem ich mich ein Teil von ihnen werde. Mit geht es um den Ist-Zustand.

Den Artikel zu dieser Idee für Frühjahr und Sommer 2013 finden Sie in der dotnetpro 5/2013 – oder hier zum bequemen sofortigen Lesen:

Ich würde mich freuen, wenn Sie mir eine Station für meine Walz anbieten würden. Meine Erfahrungen kommen am Ende der Community zugute.

Dienstag, 26. März 2013

Entwerfen fürs Schreiben

Gerade merke ich es am eigenen Leibe, wie wichtig Entwurf ist. Nein, Software entwickle ich gerade nicht. Ich schreibe Texte. Aber ich erlaube mir, meine Erfahrungen dort auf die Softwareentwicklung zu übertragen.

Dass ich viel schreibe, ist ja nichts Neues. Warum fällt mir das Thema Entwurf dazu gerade jetzt ein? Weil ich anders schreibe als sonst.

Normalerweise sind meine Texte bis 20-25 A4 Seiten lang, inklusive Code, Abbildungen, Tabellen. Und das Thema kenne ich gut. Solche Texte kann ich leicht runterschreiben. Thema überlegen – es reicht eine grobe Überschrift oder die Idee für eine Anwendung – und dann losschreiben. Der Rest ergibt sich von selbst. Der Text gliedern sich beim Schreiben von allein. Manchmal unterbreche ich das Schreiben, um zuerst etwas zu codieren, danach geht es dann umso flotter weiter. Alles kein Problem.

Aber jetzt sitze ich an zwei Texten, die sind anders. Da kann ich nicht einfach so drauflosschreiben. Meine Schreiberfahrung und Themenkenntnis gliedert den Text nicht just-in-time. Deshalb muss ich einen Gang zurückschalten. Nachdenken ist angesagt. Entwerfen.

Das Drehbuch

Der eine Text ist ein Drehbuch. Das schreibe ich mit einer Freundin zusammen. Trotzdem ist es nicht einfach. Form und Genre sind für mich neu. Zum Inhalt nur soviel: Es ist eine Liebeskomödie. (Ja, ja, ich höre schon die Lacher. Aber mir macht es Spaß, mal etwas ganz anderes auszuprobieren. Hat sich einfach so ergeben. Ob das mal verfilmt wird? Vielleicht. Erstmal interessiert mich der Prozess.)

Bei dem Dreh konnten wir nicht einfach drauf los schreiben. Wir hatten zwar eine Idee – doch für 90 oder 100 Minuten Film ist das nicht genug. Da muss man sich an den Text ranrobben. Das ist Entwurfsarbeit at its best. Dazu gibt es eine Menge Literatur. Ein Klassiker ist Syd Fields “Das Drehbuch”. Den hatte ich schon vor vielen Jahren mal gelesen… und jetzt ziehe ich endlich Nutzen daraus. Die Zeit musste einfach reif werden. (Es hilft natürlich auch, dass ich nun eine Co-Autorin mit Erfahrung in dem Metier habe.)

Und was tun wir? Wir strukturieren. Kein Satz “Drehbuchcode” ist bisher geschrieben. Stattdessen hangeln wir uns top-down heran.

  • Ebene 1: Die Idee. Wir überlegen ganz grob, worum es geht. Was ist der Kern der Story? Welche Entwicklung sollen die Protagonisten durchlaufen? Haben wir eine “Moral von der Geschicht”? Das ganze passt auf 1 A4 Seite.
  • Ebene 2: Wir verfeinern die Geschichte. Ein paar Highlights kommen auf den Zettel. Wir überlegen, was Wendepunkte sein könnten. Woraus könnten sich Konflikte speisen? Erste Charakterstudien für die wichtigsten Protagonisten. Das passt auf vielleicht 10 A4 Seiten.
  • Ebene 3.1: Nächste Verfeinerungsstufe. Jetzt überlegen wir uns einen Handlungsverlauf, der für bestimmte Entwicklungen/Phasen schon Minutenzahlen zugeordnet bekommt. Dafür gibt es sogar eine Entwurfsskizze. Das ist schon alles ziemlich detailliert. Insgesamt kommen am Ende 30-40 Seiten Fließtext heraus, das sog. Treatment. Die Geschichte wird grob in Szenen eingeteilt erzählt.

image

  • Ebene 3.2: Gleichzeitig entwickeln sich die Charaktere. Wir erfahren immer mehr über ihre Motive und ihr Vorleben. Manchmal geben wir ihnen bewusst gewisse Züge, um in der Geschichte etwas zu bewirken; aber manchmal ergeben die sich von allein. Als hätten die Figuren ein Eigenleben.
  • Ebene 4: Das Drehbuch wird geschrieben. So richtig mit Dialogen und Einstellungsbeschreibungen. Der Umfang wird ca. 90-100 Seiten haben. Aber da sind wir noch nicht.

Dass es so am besten funktioniert, habe ich immer gewusst. Aber ich habe nie selbst Zugang zu diesem Vorgehen gefunden. Insofern kann ich jetzt besser mit Ihnen fühlen, falls Sie es schwierig finden, sich auf Softwareentwurf einzulassen.

Mit ein wenig Anleitung durch meine Co-Autorin hab ich jetzt aber den Sprung in dieses Vorgehen geschafft – und es macht Spaß. Wir haben auch eine schöne Rollenverteilung gefunden. Ich bin eher der Mann fürs Grobe :-) Sie feilt dann die Details aus. Auch beim Drehbuch konzentriere ich mich also eher auf den Entwurf, das Big Picture, den Rahmen. Das liegt mir irgendwie.

Natürlich geht es am Ende wie bei Software nicht ohne Details. Die dreckige Dialogrealität will ausgefleischt werden. Aber das geht viel, viel einfacher auf der Basis eines mehrstufigen Entwurfs.

Der ist auch auf jeder Ebene ein Durchstich. Die ganze Geschichte wird immer wieder erzählt – nur eben mit unterschiedlichem Detailierungsgrad.

Das läuft natürlich nicht sauber in Wasserfallmanier von oben nach unten ab. Es ist eher Jo-Jo-Schreiben. Mal ganz oben, dann wieder kurz durchfallen auf die unterste Ebene. Wir wissen schon ein paar Einstellungen und Sätze für Dialoge, auch wenn wir eigentlich noch beim Treatment sind. Und manchmal gehen wir auch wieder auf eine höhere Ebene und drehen den Fluss der Geschichte in eine andere Richtung.

Das Fachbuch

Das Drehbuch ist Hobby. Arbeit ist das Buch über Flow-Design, an das ich mich nun endlich doch mal gemacht habe. Die Nachfrage ist gestiegen – und ich empfinde das Thema als inzwischen so stabil, dass sich das Aufschreiben lohnt.

Hier bin ich nun voll in meinem Metier. Trotzdem kann ich nicht einfach losschreiben. Dafür ist der Scope zu groß. Ein Blogposting wie dieses oder einen Artikel von 10 Heftseiten für die dotnetpro sind überschaubar. Da kann ich die Text-IDE Word aufklappen und losschreiben. Aber ein ganzes Buch… Nein, das geht nicht. Das würde zwar am Ende auch irgendwie alle Details enthalten – nur in einer nicht sehr leserfreundlichen Weise. Einen so großen Text beim Schreiben entstehen zu lassen, würde zuviel Refaktorisierungsaufwand bedeuten. Das ist wie beim Coden.

Also denke ich vorher über den Text nach. Auch auf mehreren Abstraktionsebenen.

  • Ebene 1: Grobe Gliederung. Ich unterteile das Thema in Bände. Jeden einzelnen will ich als eBook veröffentlichen, sobald er fertig ist. Es kommt also nicht alles erst am Ende als ein Buch heraus.

image

  • Ebene 2: Gliederung eines Bandes. Zu jedem Band überlege ich mir eine Kapiteleinteilung für den groben Erklärungsfluss. Das habe ich aber bisher nur für Band 1 getan. Ich möchte mich konzentrieren auf ein Inkrement. Denn nichts anderes sind die Bände. Jeder für sich bietet schon einen Nutzen.
    Ob es am Ende wirklich soviele Bände oder genau die werden? Keine Ahnung. Das ist auch nicht wichtig. Bisher ist ja kein Aufwand in Band 2 bis 5 geflossen.

image

  • Ebene 3: Ich schreibe mir zu jedem Kapitel Stichpunkte auf. Eine Sammlung von Material, das ich in den Kapitel behandeln will. Das habe ich für Band 1 auch für alle Kapitel gemacht. Hier nur ein Auszug für ein Kapitel:

image

  • Ebene 4: Ich schreibe jedes Kapitel in einem eigenen Word-Dokument. Das mache ich vor allem, um mich besser fokussieren zu können. Aber ich will damit auch technischen Problemen vorbeugen. Wer weiß, wie Word reagiert, wenn am Ende hunderte Bilder in einem Dokument sind und dessen Größe mehrere hundert MB umfasst?
    Das ist jetzt “Codieren”: Ich schreibe den Text, den Sie später lesen sollen. Aber beim Schreiben gliedere ich weiter. Das ist sozusagen situativer Entwurf. Vorher war strategischer und taktischer Entwurf. Hier die dabei entstandene Gliederung für ein Kapitel:

image

Auch dieser Prozess ist natürlich jo-jo-iterativ. Ich kann vom Schreiben jederzeit wieder in den Entwurf. Und ich kann jederzeit zwischen den Entwurfsebenen wechseln.

Der Entwurf gibt mir einen Überblick und definiert Ösen, in die ich dann Fließtext einhängen kann. Ich kann jederzeit an jeder Stelle arbeiten, weil ich weiß, wie der Zusammenhang ist. Aber ich ziehe es vor, Inkremente zu produzieren. Zuviel Springen ist auch nicht gut. Außerdem habe ich zu bestimmten Themenblöcken, die weiter hinten im Buch liegen, noch keine rechte Idee, wie sie konkreter aussehen sollten. Das mache ich auch abhängig von dem, was ich weiter vorne an Text produziere oder was mir bis dahin noch für Gedanken kommen mögen. Ich will keine Entwurfshalde auftürmen.

Auch beim Fachbuch erfahre ich also, wie wichtig es ist, vor dem Codieren, äh, Schreiben mal nachzudenken, einen Rahmen aufzuziehen, ein Big Picture zu entwickeln. Ein Plan ist gut – und ich erwarte nicht, dass der 100% eingehalten wird. Trotzdem ist er besser als keiner.

Beim Entwurf geht es nicht darum, alles komplett vorwegzunehmen. Dann wäre es kein Entwurf mehr, sondern schon Schreiben bzw. Codieren. Ein Entwurf ordnet vielmehr grob. Sein Ergebnis sind handhabbare Blöcke. Ein Kapitel ist so groß wie ein Artikel. Das kann ich ad hoc formulieren und strukturieren. Ein ganzes Buch jedoch, das ist zu groß. Dito bei Software. Eine ganze Software ist zu groß, um einfach mit dem Codieren anzufangen. Aber eine Klasse oder eine Methode, die durch einen guten Entwurf entstehen, sind überschaubar.

Es braucht daher für Software wie für Texte eine Entwurfsmethode. Das ist mir jetzt durch diese Schreiberfahrungen nochmal klar geworden.

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.

Mittwoch, 13. März 2013

Wider den sprachlichen Plattform Lock-In

One platform to rule them all – so könnte wohl das Motto vieler Softwareunternehmen/-abteilungen lauten. Man ist ein Java-Shop oder eine .NET-Bude oder eine Ruby-Schmiede usw. Hier und da machen einige Entwickler vielleicht einen Ausflug in andere Gefilde, doch das Hauptprodukt wird auf einer Entwicklungsplattform hergestellt.

Das ist kein Zufall. Das ist gewollt. Das will das Management so, weil Vereinheitlichung eine gute Sache ist. Dadurch kann man immer Geld sparen, oder? Und das wollen die Entwickler auch so, weil das weniger verwirrend ist. Es würde sich doch keiner mehr auskennen, wenn ein bisschen Code in Java, ein bisschen in C#, ein bisschen in Ruby und noch ein bisschen in JavaScript oder Clojure oder C++ vorläge. Oder?

Da ist natürlich etwas dran. Eine gewisse Vereinheitlichung, ein Fokus ist ökonomisch. Aber nur bis zu einem gewissen Grad. Darüber hinaus kippt Vereinheitlichung, d.h. Homogenität um in Monokultur mit all ihren Nachteilen.

Aus Plattform-Fokus wird dann Plattform Lock-In. Man kann dann nicht mehr anders. Die Plattform ist dann eine systemrelevante Größe. An der ist nicht mehr zu rütteln.

Das hat negative Auswirkungen auf die Evolution der Software. Die kann dann nämlich nicht mehr mit der Zeit gehen. Entwicklungen auf anderen Plattformen oder gar Entwicklungen ganz neuer Plattformen ziehen an ihr vorbei. Und oft genug sind es sogar Entwicklungen der Plattform selbst, die man nicht nutzen kann. Gerade neulich war ich wieder bei einem Team, das sich noch an .NET 2.0 bindet. Erweiterungsmethoden, Lambda-Ausdrücke, yield return, Linq, dynamische Typen, aync/await und mehr sind dort kein Thema.

Zu den negativen Auswirkungen auf die Software kommen negative Auswirkungen auf das Team. Teams verkalken, wenn sie Jahre lang auf eine Plattform starren. Sie werden immer mehr zu einer Echokammer, durch die dieselben alten Glaubenssätze und derselbe alte Code hallen. Sie werden damit immer unattraktiver für andere Entwickler.

Bei allem Verständnis für den Vereinheitlichungsreflex halte ich diese Nachteile für so offensichtlich, dass ich mich frage, warum der Plattform-Fokus landauf, landab so groß ist. Mir scheint es eine Lücke in der Argumentation dafür zu geben.

Die meine ich nun gefunden zu haben. Ich glaube, ich kenne jetzt das fehlende Argument der Entwickler für die eine Plattform. Und dieses Argumente ist keines, das sich auf einen Vorteil der einen oder anderen Plattform bezieht. Es geht vielmehr um die Kompensation eines Mangels.

Teams mangelt es an einer anderen Sprache für die Beschreibung von Software außerhalb ihrer Programmiersprache.

Oder anders herum: Ich glaube, Teams fühlen sich zu der einen Plattform hingezogen, weil deren Programmiersprache für sie das einzig verlässliche Kommunikationsmittel über Software darstellt.

Es geht weniger um technische Vorteile einer bestimmten Plattform. Es geht auch weniger um technische Vorteile durch Konzentration auf nur eine Plattform. Es geht auch nicht um personelle Vorteile durch eine Vereinheitlichung [1].

Nein, die eine Plattform dient auch als die eine Sprache. Denn wie ein Blick auf aktuelle Konferenzprogramme zeigt, gibt es keine Sprache, um über Code zu reden. Es gibt nur Code und Technologien/Frameworks. Auf der Meta-Ebene findet im Grunde nichts mehr statt.

Früher gab es die ehrwürdige Disziplin des Softwareentwurfs. Die hatte den Anspruch, eine Lösung sprach-/plattformunabhängig zu beschreiben. Zugegeben, das ist manchmal ein bisschen weit getrieben worden. Doch der Grundgedanke war richtig, finde ich. Code war “nur” ein notwendiges und nicht triviales Implementationsdetail.

Diese Disziplin hat eigene Hochsprachen entwickelt. EBNF, RegEx, Zustandsdiagramm, Struktogramm, UML-Diagramme, sogar noch Blockdiagramme mit dem Schichtendiagramm als Sonderfall gehören dazu.

Doch das ist heute alles hohles Geschwätz. Wer nicht codiert, verschwendet Zeit an Flüchtiges. Diagramme haben keinen Bestand, sie veralten in dem Moment, da man sie implementiert. Also gar nicht erst damit anfangen. Jedenfalls nicht offiziell.

Ich kenne kein Tutorial, das eine Vorgehensweise zur Entwicklung von Software von Anforderungen bis zum Code beschreibt, in dem nicht schnellstmöglich zur Tastatur gegriffen wird. Code ist Trumpf! Code ist die einzige Wahrheit! Das wird mantrahaft wiederholt. Und alles andere wird entweder gar nicht thematisiert oder rein textuell abgehandelt. Und so wird aus einem Text ein anderer. Keine Abstraktion, keine Visualisierung. Diagramme sind selten und unsystematisch [2].

Code und Infrastruktur: das ist der Inhalt des Entwicklerlebens.

Und weil das so ist, müssen Code und Infrastruktur vereinheitlicht sein. Denn sie stellen die einzige Sprache dar, auf die Verlass ist.

Was aber nun, wenn Entwickler sich üben würden, ihre Software auch anders ausdrücken zu können. UML hat dazu einen Vorschlag gemacht – leider einen zu schwerfälligen. Vielleicht geht es ja aber auch einfacher. Ich denke, die Suche nach einer leichtgewichtigen Weise zur Beschreibung von Software, ist lohnend [3]. Dabei geht es ja nicht um einen Ersatz für Programmiersprachen. Dann wäre nichts gewonnen. Leichtgewichtigkeit bedeutet Entlastung von den ganzen Details, die sich beim und durch Codieren ergeben. So wie das Codieren in heutigen Hochsprachen auch eine Entlastung ist von ekeligen Details, wie sie bei der Assembler- und Maschinencodeprogrammierung wichtig sind. Speichermanagement, Registerallokation, Kontrollstrukturenbau und vieles mehr liegt hinter uns.

Der Gewinn einer plattformunabhängigen, vom Code abstrahierenden Sprache läge für mich nicht nur in verbesserter Möglichkeit, allein oder im Team über Codestrukturen nachzudenken. Softwareentwurf könnte damit also besser skalieren. Er würde von einer persönlichen Angelegenheit, die beim TDD-Codieren passiert, zu einer Angelegenheit des Teams. Die Collective Software Ownership würde steigen.

Der Gewinn würde außerdem in einer Befreiung aus der Umklammerung einer Plattform bestehen. Endlich könnten Entwickler verschiedener Plattformen an einem Tisch sitzen und gemeinsam über Softwarestrukturen nachdenken. Die würden sie unterschiedlich implementieren, je nach Plattform. Aber sie hätten zunächst einmal eine gemeinsame Vorstellung vom bigger picture.

Eine leichtgewichtige Sprache, um über und (weitgehend) unabhängig von Code zu “reden”, würde die Softwareherstellung also aus dem Plattform Lock-In befreien. Endlich könnte Software leichter aus Code auf verschiedenen Plattformen zusammengesetzt werden. Und das würde die Evolvierbarkeit von Codebasis und Team beflügeln.

Expliziter Entwurf in einer Entwurfssprache ist kein Selbstzweck. Mir scheint er vielmehr eine Bedingung für die Möglichkeit nachhaltiger Softwareentwicklung.

Medizin, Soziologie, Psychologie, Land- und Forstwirtschaft und Politik haben erkannt, dass Monokulturen mittelfristig schädlich sind. Überall da, wo es um Leben, um Entwicklung geht, fördert Vielfalt dieses Ziel. Nur bei der Software-Entwicklung glaubt man noch Gegenteiliges. Da setzt man noch auf Homogenisierung, Monokultur, Vereinheitlichung, Konsolidierung.

Damit sollten wir aufhören. Dafür ist aber eine praktikable Entwurfssprache und –methode nötig. Daran sollten wir arbeiten. Dazu brauchen wir mehr Ideen [4].

Endnoten

[1] Eine Plattformvielfalt kann personell nur von Vorteil sein. Nicht nur würde sie Diversität im Denken fördern, was inzwischen allgemein anerkannt einen immer größerer Überlebensvorteil für Unternehmen darstellt. Noch konkreter bedeutet Plattformvielfalt eine viel größere Auswahl an Entwicklern.

Wer nur Java oder C# macht, der kann nur aus der Java- oder C#-Community rekrutieren. Wer aber Java und C# und Python und JavaScript und Ruby macht, der kann in all diesen Communities nach neuen Teammitgliedern suchen.

Wo Plattformvielfalt Programm ist, gibt es keine zähneknirschende Einstellung plattformfremder Entwickler mehr, die dann mühsam auf die eigene Plattform umgehoben werden müssen. Dann kann jeder mit seiner Plattform zum Gelingen der Software viel schneller beitragen. Das spart bares Geld!

[2] Hier uns da tauchen mal UML-Diagramme auf: ein Klassendiagramm, ein Sequenzdiagramm. Auch Blockdiagramme gibt es hier und da. Doch auch wenn deren Syntax definiert sein mag, ihre Nutzung ist unsystematisch. Sie ist punktuell, steht in keinem Zusammenhang, ist nicht Teil einer Methode oder übergreifenden Sprache oder Kultur.

Diagramme werden nicht als gleichberechtigtes Mittel neben Code angesehen. Sie sind eher Notlösungen, wenn man grad nicht anders kann.

[3] Natürlich wäre es auch gut, wenn Entwickler sich mit mehr Programmiersprachen beschäftigen würden. Dann wären andere Plattformen ihnen nicht so fremd; sie wären offener, für manche Funktionalität auch mal etwas anderes auszuprobieren.

Dass der wahrhaft polyglotte Programmierer jedoch bald Realität würde, sehe ich nicht. Genauso wenig wie Menschen, die mehr als zwei Fremdsprachen sprechen, selten sind. Bei den natürlichen Sprachen hat sich deshalb eine Universalsprache zur kulturübergreifenden Verständigung durchgesetzt: Englisch.

Allerdings ist das kein Englisch, das alle Feinheiten der englischen Sprache enthält, sondern eine Verflachung. Allemal das gebrochene gesprochene Englisch. Doch das reicht für die meisten Situationen, in denen viele Details, Feinheiten, Nuancen ausgelassen werden können.

Warum nicht genauso in der Softwareentwicklung? Womit natürlich keine verflachte 3GL gemeint ist, sondern eine echte Abstraktion.

[4] Wie gesagt, die UML hat einmal versucht, so etwas zu sein. Doch sie ist gescheitert. Ich würde sagen, an Hybris. Sie hat sich nicht beschränken wollen in Breite und Tiefe. Oder es waren zu viele Köche an diesem Brei beteiligt?

Das bedeutet nicht, dass die UML nichts verwertbares enthielte. Ich sehe aber nicht UML 3.0 als Lösung, sondern eher etwas anderes, das hier und da aus der UML etwas entlehnt, ansonsten aber eigenständig ist.