• Teaser Home

    Clean Code Developer School

    Saubere Softwareentwicklung üben – regelmäßig, fokussiert, individuell, angeleitet

Der Papertrail eines Trainings

„Wie läuft eigentlich ein Clean Code Development Training ab?“ werden wir immer mal wieder gefragt. Dann versuchen wir zu beschreiben, wie wir mit Übungen und Diskussion und am Flipchart arbeiten.

Clean Code Development braucht hands-on experience, als Trainingsteilnehmer muss man „es“ getan haben. Und zwar nicht zu knapp, in vielen Wiederholungen. Immerhin gilt es, alte Gewohnheiten abzulegen, die fast schon automatisiert in den Fingerspitzen stecken.

Aber auch wenn es um Clean Code geht, dreht sich das Training nicht nur ums Codieren. Damit ließe sich der Code gar nicht genügend sauber gestalten. Wer erst beim Codieren an Clean Code denkt, denkt zu spät daran.

Deshalb müssen wir viel Reden. Clean Code ist das Ergebnis von Diskussion im Team. Auch das will geübt werden. Nein, insbesondere das will geübt werden. Denn „im Reden“ sind Softwareentwickler gewöhnlich nicht so gut. Oder allgemeiner: Im Nachdenken über Lösungen vor dem Codieren sind sie nicht so geübt, vor allem im kollektiven Nachdenken.

Das tut aber Not. Denn wenn am Ende Collective Code Ownership stehen soll, dann reicht es nicht, den Code im Pair Programming zu schreiben. Dazu müssen Reviews kommen (Diskussion!), dazu müssen aber vor allem gemeinsame (!) Analyse der Anforderungen und gemeinsames (!) Nachdenken über den Lösungsansatz kommen (Diskussion!).

Und da solches diskursives Nachdenken nicht so schnell geht und für die meisten Teilnehmer sehr ungewohnt ist, verwenden wir den größten Teil von Clean Code Development Trainings darauf.

Anders als im bisher üblichen Tagesgeschäft der Teilnehmer sind wir allerdings sehr darauf bedacht, dass dieses gemeinsame Nachdenken nicht spurlos bleibt. Dafür hier ein Beispiel aus einem aktuellen Training:

Das ist der Papertrail von zwei Tagen. In denen haben wir uns auf eine Übungsaufgabe konzentriert: eine Anwendung zur Vorhersage von Aufwänden mittels Monte Carlo Simulation.

Was genau all diese Diagramme und Notizen bedeuten, ist hier nicht so wichtig. An dieser Stelle möchte ich Ihnen nur einen visuellen Eindruck davon vermitteln, wie es halt in den Trainings der CCD School zugeht.

Acht Teilnehmer und ich haben im Verlauf von zwei Tagen, also 16 Stunden Training, diese Spur in intensiven Diskussionen produziert – und auch noch implementiert.

Am ersten Tag haben wir ca. 60 Minuten codiert; wir hatten uns nur ein kleines Inkrement zum Einstieg vorgenommen. Am zweiten Tagen waren es 190 Minuten in mehreren Blöcken und sogar arbeitsteilig: ein Teil der Teilnehmer hat sich auf die Simulation konzentriert, ein anderer auf die Vorhersage. Beide Aspekte der Domäne wurden dann als Bibliotheken den anderen zur Integration zur Verfügung gestellt.

So geht es zu in unseren Clean Code Development Trainings. Selbst Männer kommen ins Reden – und fühlen sich gut dabei :-) Und damit nicht nur „daher geredet wird“, zeichnen wir die Gedanken auf. Es ist uns wichtig, dass die Teilnehmer lernen, das, was sie in der Analyse verstehen, anderen visuell vermitteln zu können. Nur so lässt sich ein gemeinsames (!) Verständnis verlässlich erreichen. Dasselbe gilt für ihre Vorstellungen von der Lösung des Problems. Visualisierung, Gedanken auf Papier ausdrücken können, ist das A und O der Softwareentwicklung im Team. Dazu kommen natürlich noch Prinzipien und Konzepte sauberer und agiler Strukturen, doch das Codieren ist am Ende im Grunde der einfachste Teil. Clean Code schreibt sich fast schon von allein ;-)

Wenn Sie jetzt Lust bekommen haben, das auch einmal zu erleben, dann schreiben Sie uns eine Email an info@ccd-school.de und wir schauen, wann wir für Ihr Team das „Trainingslager“ aufschlagen können. Oder Sie machen bei einem Clean Code Retreat mit oder belegen einen Kurs der Clean Code Development Akademie.

Kontrollstrukturen in der Integration

Die beiden wesentlichen Prinzipien für Clean Code Development sind für uns das IOSP und das PoMO.

  • Nach dem Integration Operation Segregation Principle (IOSP) sollen Funktionen (oder allgemeiner: Module) ihre Grundverantwortlichkeit entweder auf die Herstellung von Verhalten durch Logik konzentrieren (Operation) oder selbst keine Logik enthalten, sondern nur andere Module zu einer größeren Einheit integrieren.
  • Das Principle of Mutual Oblivion (PoMO) hingegen sorgt für Entkopplung von Funktionen (oder allgemeiner: Modulen), die zusammen einen Gesamtzweck verfolgen. Sie sollen einander nicht kennen, sondern lediglich über Nachrichten anonym und unidirektional in Verbindung stehen. Die Verschaltung zu einem Netzwerk übernimmt eine integrierende Einheit.

Diese Prinzipien sind klar und einfach und führen zu Code, der einige günstige Eigenschaften besitzt, z.B.:

  • Funktionen haben nur einen geringen Umfang.
  • Funktionen sind quasi selbstverständlich auf eine inhaltliche Aufgabe konzentriert.
  • Funktionen sind nicht funktional von einander abhängig. Das macht sie leicht testbar.
  • Integrationen bieten natürliche Ansatzpunkte für Erweiterungen, ohne bestehende Logik verändern zu müssen.
  • Integrationen geben in leicht lesbarer Form das Was von Code wieder.

Integration ohne Logik

Allerdings stößt die Befolgung dieser Prinzipien immer wieder auf Widerstände, weil es schwierig oder gar unmöglich scheint, Logik komplett aus Integrationen herauszuhalten. Wie soll z.B. dieser Entwurf umgesetzt werden?

Eine Eingabe, die entweder eine römische Zahl oder eine Dezimalzahl ist, soll in die jeweils andere Zahlendarstellung übersetzt werden. Dazu ist doch eine Fallunterscheidung nötig. Die naheliegende Übersetzung wäre:

Dieser Code integriert To_roman() und From_roman(), aber er enthält auch Logik: die if-else-Anweisung und die konkrete Transformation des Input in einen bool-Wert.

Das widerspricht jedoch leider dem IOSP. Nur wie sollte es sonst aussehen?

Um die Integration konsequent frei von Logik zu halten, sollte so verfahren werden:

Jetzt findet nur noch Integration statt. Die komplette Entscheidungslogik ist in Classify() gekapselt. Wie festgestellt wird, ob eine Dezimalzahl oder eine römische anliegt, ist nicht mehr zu sehen.

Das technische Mittel hinter dieser Übersetzung sind Funktionszeiger, die als Continuation eingesetzt werden:

Nicht nur dient diese Funktion der Einhaltung des IOSP, sie zeigt auch PoMO-Konformität: indem die Parameternamen für die Continuations auf die Funktion selbst, also nach innen bezogen sind, wird keine Annahme darüber gemacht, was mit den Daten passiert, die über sie hinausfließen. Die Funktion hat kein Wissen über einen eventuellen Downstream ab ihrer Position in einem Verarbeitungsfluss.

Aber auch wenn auf diese Weise sehr flexibel Teilungen im Datenfluss oder mehrfache oder verzögerte Output-Lieferungen einer Funktionseinheit möglich sind… sie erweist sich stets als gewöhnungsbedürftig.

Gerade dem Clean Code Neuling stellt sich daher die Frage: Ist denn diese Art der Übersetzung wirklich alternativlos, wenn man das IOSP einhalten will? Darf in einer Integration wirklich, wirklich gar keine Logik stehen?



Integration mit Kontrollstrukturen

Ja, wir sind der Meinung, dass Integration vollständig ohne Logik gedacht und implementiert werden sollte. Zumindest zunächst. Zur Übung. Um sich dem Imperativen, dem Kontrollflussdenken zu entwöhnen, das der Objektorientierung unterliegt. Denn mit Kontrollflüssen lassen sich keine Lösungen planen. Die Darstellung von Kontrollflüssen „skaliert nicht“. Sie greift auch zu schnell auf globalen Zustand zurück und erzeugt Abhängigkeiten.

Am Ende jedoch liegt es uns fern, ein Dogma in die Welt zu stellen. Alles sollte mit Augenmaß und moderat betrieben werden, auch die Einhaltung von klaren Prinzipien. Solange dem Zweck gedient ist, ist auch Logik in Integrationen erlaubt. Und was ist der Zweck?

  • Leichte Verständlichkeit
  • Leichte Testbarkeit
  • Hohe Wandelbarkeit

Fallunterscheidungen

Hier ein erster Vorschlag zur Güte mit Logik in der Integration zum obigen Beispiel:

Die Integration enthält immer noch if-else als Kontrollstruktur, doch das Wie der Entscheidung ist ausgelagert in die Funktion Is_decimal(). Die ist wieder gut testbar.

Allerdings weicht der Name der entscheidenden Funktion nun vom Entwurf ab. Das ist dem Rückgabewert geschuldet. Da die Wahl auf bool gefallen ist zur Unterscheidung der beiden Zahlenarten, passt Classify() als Funktionsname nicht gut; if (Classify(input)) liest sich nicht flüssig.

Natürlich ist die Verwendung von bool als Rückgabetyp ok, doch dann sollte der Entwurf anders aussehen. Schon dort sollte „Is decimal?“ als Bezeichnung für die Funktionseinheit gewählt werden, die die Zahlenart prüft.

Einerseits. Denn andererseits sollte ein Datenfluss eben Datenfluss sein und nicht Kontrollfluss. Ein if jedoch ist die grundlegende Anweisung in Kontrollflüssen. Es ist direkt vom bedingten Sprung des Maschinencodes abgeleitet.

Die Bezeichnung „Classify“ im obigen Entwurf war also schon mit Bedacht gewählt. Denn darum geht es: eine Klassifikation, aufgrund derer die Verarbeitung unterschiedlich weiter fließt.

Wenn dieser Name beibehalten werden soll, wie könnte dann dafür eine Übersetzung aussehen? Ich schlage vor, eine Aufteilung des Datenflusses allgemein mit einem enum-Datentyp anzuzeigen:

Aus dem if-else wird dann ein switch:

Das sieht ähnlich der Lösung mit den Continuations aus, oder? Es ist sogar ein wenig besser lesbar, da für jeden Ast des Datenflusses klar ist, um welchen es sich handelt.

Ist Logik in der Integration also doch eine gute Sache und erlaubt? Jein. So, wie hier angewandt, ist sie unschädlich, gar hilfreich. Doch unbedacht angewandt, öffnet sie Tür und Tor zu schlechter Lesbarkeit und schlechter Testbarkeit.

So auch hier: Es lässt sich zwar leicht testen, ob Classify() seine Sache korrekt macht. Aber wird das Ergebnis dann auch korrekt weiterverarbeitet? Gibt es einen Fall für jeden Wert des enum? Der C#-Compiler stellt das nicht sicher. Um die Abdeckung zu prüfen, müsste also im Zweifelsfall die switch-Logik in der Integration getestet werden – mit all dem, was daran hängt. Die Testbarkeit des Codes ist damit geringer, als würde das switch (oder ein if) in Classify gekapselt sein.

Schleifen

Wie viel Logik ist denn nun aber erlaubt? Bevor ich die Frage beantworte, lassen Sie uns noch auf die Schleifen als Kontrollstrukturen schauen.

In Datenflüssen werden häufig Mengen von Daten übermittelt. Hier ein Beispiel:

Aus einer Pfadangabe werden viele Dateien, die jede in ein Resultat zu überführen sind. Dass nicht nur ein Datum fließt, sondern eine Collection von Daten, zeigt der * hinter der Datenbezeichnung in der Klammer an.

Wenn die Verarbeitung pro file einfach ist, dann ist Process_files() eine Operation, z.B.

Doch was, wenn die Arbeit auf dem Dateiinhalt umfangreicher ist? Dann wäre es doch angezeigt, die in eine eigene Funktion auszulagern, oder? Das jedoch würde zu einem Aufruf dieser Methode in Process_files() führen, wo schon eine Kontrollstruktur steht. Ein Widerspruch zum IOSP.

Eigentlich. Denn dieser Code ist gut lesbar. Warum sollte er also nicht erlaubt sein? Die Testbarkeit ist nicht geringer geworden. Denn dass ein foreach falsch angewandt wird, ist eher nicht zu erwarten. Also kann sich ein Test auf die Operation konzentrieren, die sich mit einem file befasst: Process_file.

Die Kombination aus „Pluralfunktion“ und „Singularfunktion“ ist typisch für Implementationen von Datenflussentwürfen. Im Entwurf steht nur die Verarbeitung einer Collection, im Code wird das jedoch mit einer Funktion für diese Funktionseinheit plus einer weiteren zur Verarbeitung eines einzelnen Collection-Elements realisiert: Process_files() + Process_file().

Und wie steht es dann mit for()– und while()-Schleifen?

Wir raten davon ab!

Eine for-Schleife sieht unschuldig aus:

Doch im Gegensatz zu einer foreach-Schleife steckt hier Logik drin und kann auch nicht weiter versteckt werden. Ist es korrekt, bei Index 0 zu beginnen? Ist die Bedingung i < files.Length korrekt? Soll wirklich nach jeder Runde der Index um 1 hochgezählt werden? Und wenn ja, wird das auch so getan?

In diesem Fall ist die Logik der Schleife simpel, doch das kann sich schnell ändern. Wie testen Sie dann, ob sie (noch) korrekt ist? Das ist isoliert nicht möglich wie beim switch mit seinen Fällen.

Während beim switch jedoch Zählen hilft, ob alle enum-Werte mit einem Fall bedacht sind, ist die Analyse der Logik einer for-Schleife nicht so einfach. Also: Finger weg von for-Schleifen in Integrationen! Sie sind zu wenig deklarativ.

Dasselbe gilt für while-Schleifen, allerdings aus anderem Grund. Schauen Sie hier:

Spüren Sie es auch: Das ist viel weniger übersichtlich. Selbst die for-Schleife mit ihrer Logik war besser verständlich.

Immer noch wird ein Dateiinhalt verarbeitet und das Resultat zurückgegeben. Doch drumherum passiert einiges an Logik, das der while-Schleife dient. Nicht nur steht bei while ein Vergleich, es muss auch noch eine Hilfsvariable deklariert und aktualisiert werden. Deshalb auch das explizite result. (Die Queue<> als Quelle habe ich nur gewählt, um nicht noch weitere Logik zu brauchen, um auf die einzelnen Dateiinhalte zuzugreifen. Sie hat ansonsten keine Bedeutung.)

Beim while steckt die Logik nicht nur in einer Klammer, sondern verteilt sich über die ganze Schleife. Die Verantwortlichkeit ist verschmiert und vermischt mit der Integration. Sie zu verstehen, ist nicht auf einen Blick möglich, von der Testbarkeit ganz zu schweigen.

Das lässt sich auch nicht heilen, indem die Schleifenbedingung wie beim if in eine eigene Funktion ausgelagert wird. Es braucht weiterhin Zustand, der während der Schleife aktualisiert wird. Also: Finger weg von while-Schleifen in Integrationen.

Und wie sollte das obige Szenario stattdessen aussehen? Eine while-Schleife kann natürlich zum Einsatz kommen – allerdings nur in einer Operation.

In der Integration Process_files() findet nun wirklich nur Integration statt; jegliche Logik ist verschwunden. Calculate() ist fokussiert auf die Berechnung der Resultate. Und die Schleife ist nach Limit() ausgelagert. Dort wird nun jedoch keine weitere Produktionsfunktion mehr aufgerufen. Limit() enthält ausschließlich Logik, sie ist eine Operation.

Bitte beachten Sie, wie der Wunsch, das IOSP einzuhalten, dazu geführt hat, dass die Verantwortlichkeit, die in der while-Schleife steckte, nun freigestellt und mit einem Namen versehen ist. Es geht um eine Begrenzung der Zahl der Resultate. Das war vorher nur durch umständliche Interpretation erahnbar, als Logik und Integration noch vermischt waren.

Da die Operation Limit() ohne funktionale Abhängigkeiten gut testbar ist, können Sie sich jetzt auch leichter entscheiden, sie umzuschreiben. Vielleicht gefällt Ihnen das while nicht und sie wollen stattdessen ein foreach mit einem if darin für vorzeitigen Abbruch?

Rekursion

Schleifen lassen sich durch Rekursionen ersetzen und umgekehrt. Deshalb ist auch ein Wort nötig zur Verwendung von Logik in rekursiven integrierenden Funktionen.

Rekursionen müssen abgebrochen werden. Irgendwo ist eine Fallunterscheidung nötig, das ist Logik. Wird jedoch nicht abgebrochen, dann erfolgt der Aufruf einer Funktion, das ist Integration. Rekursionen erfordern daher zwangsläufig eine Mischung aus Logik und Integration.

Wenn die Abbruchentscheidung jedoch deutlich sichtbar z.B. am Anfang der integrierenden Methode steht, ist dagegen nichts auszusetzen. Verständlichkeit und Testbarkeit leiden nicht.

Die rekursive Variante von Process_files() enthält zwei integrierende Schritte. Dass davor noch eine Zeile für die Abbruchbedingung steht, behindert die Verständlichkeit nicht. Aber natürlich gilt auch hier: Die Logik der Abbruchbedingung sollte für sich testbar sein (oder trivial wie im obigen Fall).

Empfehlungen

Wie sollten Sie mit Logik in Integrationen verfahren? Die Empfehlung lautet weiterhin: Halten Sie Integrationen frei von Logik!

Wenn Sie aber gar keinen Ausweg mehr sehen oder die Alternative wirklich nicht einfach zu lesen ist, dann beherzigen Sie Folgendes in der Integration:

  1. Benutzen Sie nur foreach und nicht (!) for oder while als Schleifen.
  2. Benutzen Sie if nur mit einer eigenen Funktion für die Bedingung.
  3. Benutzen Sie switch nur mit einer eigenen Funktion für die Fallbestimmung und liefern Sie aus der einen enum-Wert zurück.
  4. In einer Rekursion machen Sie die Abbruchbedingung so knapp wie möglich (s.o. if bzw. switch).
  5. Benutzen Sie pro Integration höchstens eine Kontrollanweisung.
  6. Schachteln Sie in Integrationen keine Kontrollanweisungen.

Das sind wirklich gut gemeinte Ratschläge. Sie sollen Ihnen helfen, dem Prinzip IOSP möglichst treu bleiben zu können.

Doch am Ende ist das Problem mit allen Empfehlungen, dass sie entweder zu Widersprüchen führen oder in sklavischer Anwendung das Gegenteil von dem erzeugen, was beabsichtigt war.

Deshalb zum Schluss noch einmal: Setzen Sie programmiersprachliche Mittel ein, wie Sie wollen. Nur überprüfen Sie immer wieder, ob Verständlichkeit und Wandelbarkeit hoch gehalten werden. Die Idee hinter IOSP (und PoMO) ist, dass durch die Abwesenheit von funktionalen Abhängigkeiten ein Datenfluss entsteht, der sowohl das gewünschte Verhalten zeigt wie auch die genannten nicht-funktionalen Anforderungen erfüllt. Das ist es wert, auch ab und an mal eine ungewohnte Codeformulierung in Kauf zu nehmen.

Struktur folgt Kräften

Strukturen sind, wie sie sind, weil das ihrem Zweck am besten dient. Das gilt für Gebäude, Brücken, Fahrzeuge, Organisationen, Organismen.

In der Natur entstehen Strukturen des Lebendigen in einem blinden, evolutionären Prozess. Was überlebt, hat eine genügend gute Struktur. Bei menschengemachten Strukturen sind Strukturen so gut, wie es nach Stand der Erkenntnisse möglich ist. Wie viel Erfolg sich damit erzielen lässt, zeigt die Anwendung.

Langlebig bzw. erfolgreich sind Strukturen, wenn Elemente und ihre Anordnung ein zweckvolles Gleichgewicht herstellen in einem Spiel von Kräften. Bei allem Materiellen sind diese Kräfte zunächst physikalisch. Gravitation, Wind, Erschütterungen, Wärme usw. wirken aus allen Richtungen. Dazu kommen bei menschengemachten materiellen Strukturen noch funktionale und nicht-funktionale Anforderungen. Sie formen durch Nachfrage an den Hersteller.

Bei Software ist das nicht anders. Software ist zwar eine nicht-materielle Struktur, doch auch sie wird durch Kräfte geformt. Kunden bzw. Anwender ziehen an ihr mit den Kräften Funktionalität und Effizienz. Sie stellen die Verhaltensanforderungen dar. Sie formen die Oberfläche von Software sowie ihre Verteilung auf Hosts, d.h. die interne Struktur.

Welche Menüpunkte und Buttons eine Benutzerschnittstelle bietet, ob die Software auf mehreren Threads läuft oder als Client und Server in mehreren Prozessen oder gar auf mehreren Maschinen… das und mehr ist eine Folge der Verhalten fordernden Kräfte.

Von selbst bringt sich Software aber natürlich nicht in eine zweckvolle Struktur, bei der diese Kräfte in Balance sind. Das geschieht nur, wenn diese Kräfte gegenüber den Softwareproduzenten auch kraftvoll auftreten. Sie müssen mit zwei Beinen und einem klaren Willen ausgestattet sein, von einem Hebel – z.B. Geld – ganz zu schweigen.

Und schließlich muss für alle Seiten klar erkennbar sein, ob sich die Strukturen in die Richtung der wirkenden Kräfte bewegen. Die Konformität muss messbar sein.

Das ist für Verhaltensanforderungen quasi per definitionem der Fall.

Doch der Kunde will mehr. Eigentlich.

Er möchte z.B. korrekte Software. Software soll bei Lieferung Verhalten fehlerfrei sein und diese Korrektheit über die Zeit und durch alle Veränderungen hindurch nicht verlieren. Was korrekt war, soll korrekt bleiben, er will Regressionssicherheit.

Er möchte auch Software, die sich weitreichend verändern lässt, er möchte Wandelbarkeit. Die Korrektur von Fehlern und die Erweiterung des Verhaltens sollen möglichst einfach sein und über die Lebenszeit der Software auch bleiben.

Korrektheit, Regressionssicherheit und Wandelbarkeit sind Kräfte, die nur eigentlich auf Software wirken, weil es ihnen an Klarheit und Messbarkeit fehlt. Deshalb weiß der Kunde auch nicht so genau, wie er sie auf die Softwareproduktion anwenden soll.

Er kann die Abwesenheit von Fehlern nur bedingt aktiv feststellen. Meist erleidet er sie nur zur Unzeit. Dann jedoch kann er sie ummünzen in Forderungen nach Funktionalität oder Effizienz.

Um die Wandelbarkeit steht es noch schlechter. Sie lässt sich „im Moment“ gar nicht messen, sondern zeigt sich nur im Verlauf längerer Zeit. Eine Forderungshöhe lässt sich hier jedoch gar nicht angeben.

Korrektheit und Regressionssicherheit üben mithin lediglich eine indirekte Kraft aus, Wandelbarkeit ist sogar nur eine sehr, sehr schwache Kraft.

Dementsprechend ist Software strukturiert. Ihre Struktur richtet sich vor allem nach Funktionalität und Effizienz, nur bedingt nach Korrektheit und Regressionssicherheit und quasi gar nicht nach Wandelbar.

Das ist auch völlig ok – wenn Software nur kurzlebig sein soll. Doch das ist tendenziell nicht der Fall. Software soll vielmehr unbestimmt lange leben, d.h. im Einsatz sein und sich auch noch an wandelnde Verhaltensanforderungen anpassen lassen.

Um das zu erreichen müssen die Kräfte Korrektheit, Regressionssicherheit und Wandelbarkeit verstärkt werden. Sie müssen gleichziehen mit Funktionalität und Effizienz. Dann wird sich eine neue, eine nachhaltige Balance in der Softwarestruktur zwischen allen Kräften einstellen.

Und wie lassen sich die Kräfte stärken? Durch Klarheit und Messbarkeit. Es muss sehr leicht erkannt werden können, ob eine Struktur ihnen folgt.

Ob eine Software der Kraft Funktionalität folgt, zeigt die Anwesenheit von Logik, die die Funktionalität liefert: „Dort stehen die Berechnungen, die aus den Eingaben in diesem Dialog die Grafik in jenem Dialog machen.“

Ob eine Software der Kraft der Effizienz folgt, zeigt die Anwesenheit von Logik und/oder ihre Verteilung auf Hosts: „Dort ist der Code für die Autorisierung.“, „Hier ist der UI-Thread und dort der Thread für die Transformation im Hintergrund, damit das UI nicht einfriert.“

Welche Strukturen zeigen an, dass den Kräften Korrektheit, Regressionssicherheit und Wandelbarkeit gedient ist?

Ja, ich meine Strukturen, die das sichtbar, fühlbar machen. Prinzipien sind geduldig. Software soll z.B. dem Single Responsibility Principle (SRP) folgen – doch wie kann ich das am Code ablesen?

Ohne hard-and-fast rules zur Strukturierung von Software sind die Nachhaltigkeitskräfte im Verhältnis zur den Verhaltenskräften schwach. Sie müssen schon ohne die zwei Beine des Kunden auskommen. Dann sollte zumindest für Entwickler glasklar sein, was zu tun ist.



Struktur resultierend aus der Kraft Korrektheit

Der Kraft Korrektheit wird aus meiner Sicht mit der Ausprägung eines Paares bestehend aus Testfunktion und Produktionsfunktion entsprochen.

Die Testfunktion codiert ein Beispiel für korrektes Verhalten, wie der Kunde/Anwender es sich wünscht. Sie stellt einen Akzeptanztest dar, sie dokumentiert das Verständnis des Problems. Motto: Keine Interaktion ohne automatisierten Test!

Dem entspricht im Produktionscode genau eine Funktion, die dieses Verhalten herstellt bzw. zeigt. (Natürlich dürfen auch mehrere Testfunktionen die Produktionsfunktion „auf den Grill legen“.) Sie beschreibt einen Entrypoint in die Software. Über sie wird das Verhalten durch einen Reiz aus der Umwelt ausgelöst.

Dieses Funktionspaar erreicht zweierlei: Zum einen entsteht klare accountability. Einem geforderten Verhalten wird ein konkretes Strukturelement gegenübergestellt, das dafür verantwortlich ist. Die Produktionsfunktion hat einen syntaktischen Kontrakt über den sie eine Semantik verspricht. Beides überprüft die Testfunktion. Sie fordert das Versprechen des Produktionscodes automatisiert und damit für jeden jederzeit überprüfbar ein.

Zum anderen entsteht eine Trennung zweier grundlegender Verantwortlichkeit: Benutzerschnittstelle und Verhaltensproduktion. Die Benutzerschnittstelle stellt Verhalten nicht selbst her, sondern dient nur der Veranlassung bzw. dessen Projektion. Also sollte die Benutzerschnittstelle auch nicht für die Überprüfung korrekten Verhaltens nötig sein. Sie kann die Verifikation nur erschweren entweder durch die Notwendigkeit zur manuellen Akzeptanztests oder Mehraufwand für spezielle Testautomatisierungswerkzeuge.

Software wird durch diese simple Regel sowohl horizontal wie vertikal fundamental geteilt:

  • Horizontal zerfällt Software in eine dünne Membran für das UI und einen Kern „für den Rest“.
  • Vertikal wird „der Rest“ geteilt in Funktionsbäume, deren Wurzeln für die Verarbeitung einzelner Reize stehen.

Struktur resultierend aus der Kraft Regressionssicherheit

Regressionssicherheit wird zum Teil schon hergestellt durch die automatisierten Akzeptanztests. Letztlich müssen die jedoch lückenhaft bleiben. Genügend viele Ausführungspfade lediglich durch (umfangreiche) Akzeptanztests auf Produktionsfunktionen nahe der Oberfläche abdecken zu wollen, scheint mir unrealistisch.

Zusätzlich sollten deshalb alle öffentlichen Funktionen von Produktionsklassen ebenfalls mit mindestens einer Testfunktion gepaart werden. Diese Funktionen definieren Kontrakte im Inneren von Software dar. Wie die Semantik dieser Kontrakte aussieht und ob sie eingehalten wird, ist keine geringe Sache. Beides wird aber dokumentiert mit Tests.

Müssen es wirklich alle öffentlichen Funktionen von Produktionsklassen sein? Der Einfachheit halber sage ich mal Ja. So sollte der Default aussehen. So ist die Regel einfach einzuhalten und zu überprüfen.

Im Einzelfall kann man sich natürlich begründet auch dagegen entscheiden. Außerdem ist damit nicht automatisch garantiert, dass wirklich, wirklich ein lückenloses Netz gewoben wird, das Regressionen fängt.

Mir geht es jedoch nicht um die optimale Methode, auf die Kraft Regressionssicherheit zu antworten, sondern um eine pragmatische. Klarheit, Einfachheit in der Anwendung finde ich wichtig. Für mich ist daher automatisierter Test aller öffentlichen Produktionsfunktionen der Standard und eine Abweichung ist erklärungsbedürftig. Wie es um Regressionssicherheit steht, wenn der Standard anders herum lautet, können wir an den heutigen Codebasen ablesen.

Dazu kommt, dass Testfunktionen für Entrypoint-Funktionen und andere Produktionsfunktionen zuerst geschrieben werden. Das erhöht die Wahrscheinlichkeit, dass die Tests überhaupt entstehen. Das befördert aber vor allem das Verständnis für den zu schreibenden Produktionscode. Wenn ich mir zuerst einen Test ausdenken muss, überlege ich genauer, wie der Kontrakt des Produktionscodes überhaupt aussehen soll.

Das bedeutet nicht, dass es keine Iterationen geben darf. Zumindest ist jedoch der Default klar. Wieder eine Regel, deren Einhaltung leicht überprüft werden kann.

Zunächst führt das zu keiner besonderen Struktur. Es werden eben nur noch weitere Produktionsfunktionen mit einem Test versehen. Das ändert sich jedoch, wenn man die Regel umkehrt: Wenn etwas „testwürdig“ ist, wenn ein Verhaltensaspekt sehr gezielt dokumentiert werden soll, dann müssen die zugehörigen Funktionen public gemacht werden. Und da deshalb bestehende Kontrakte nicht „verwässert“ werden sollten, führt das zur Entstehung weiterer Klassen.

Struktur resultierend aus der Kraft Wandelbarkeit

Auf die Kräfte Korrektheit und Regressionssicherheit lautet die Antwort Testfunktion. Auf die Kraft Wandelbarkeit jedoch lautet die Antwort „nur“ Testbarkeit.

Bei Korrektheit und Regressionssicherheit sichern die Testfunktionen zu, dass Verhalten existiert und sich nicht verändert. Sie sind Ausdruck dessen, was schon ist. Bei Wandelbarkeit hingegen geht es um ein Potenzial. Das braucht zwar eine Struktur, doch das kann nicht zur Laufzeit überprüft werden.

Wenn ich mir die Prinzipien für Clean Code anschaue, dann scheinen mir zwei zentral: DRY und HCLC (High Cohesion, Low Coupling). Doch zu welchen Strukturen führen die? Kann ich die Einhaltung an ihrer Form erkennen?

Man kann über diese Prinzipien und andere lange, lange nachdenken und diskutieren. Man kann sich daran richtig einen Wolf entwerfen oder refaktorisieren. Dann ist das Ergebnis vielleicht auch perfekt – nur ist das ein langsamer Prozess. Außerdem klingt das sehr kompliziert. Viel, viel Erfahrung ist nötig, um die besten ausbalancierten Entscheidungen zu treffen, oder? Wer traut sich das schon im Sperrfeuer des Alltags zu?

Diese ganze Prinzipienreiterei hört sich für mich zunehmend an wie scholastische Disputation im Mittelalter. Total interessant, nur wenig praxistauglich. Deshalb findet Clean Code Development auch so selten statt. Es scheint zu sophisticated, nichts für normalsterbliche Entwickler. Schön zum Anschauen auf Youtube und in Blogs, aber nicht zum Anwenden in der eigenen Praxis.

Wir sollten es deshalb aufgeben, perfekten Clean Code zu produzieren. Wir sollten auch aufhören, ein schlechtes Gewissen zu haben, wenn wir keinen perfekten Clean Code hinbekommen. Stattdessen lieber eine realistische, pragmatische Haltung einnehmen. Hard-and-fast rules, um jederzeit das halbwegs Richtige zu tun. Wenn dann irgendwann nochmal Zeit ist, kann man ja nachjustieren, abschleifen und polieren.

Für mich stehen deshalb zwei ganz, ganz einfache Regeln zur Herstellung von Wandelbarkeit am Anfang:

  • Produktionsfunktionen enthalten entweder Logik und rufen keine anderen Produktionsfunktionen auf; dann heißen sie Operationen. Oder sie rufen ausschließlich Produktionsfunktionen auf und enthalten keine Logik; dann heißen sie Integrationen. Ich nenne das mal the Integration Operation Segregation Rule (IOSR).
  • Außerdem sollen Produktionsfunktionen – Operationen wie Integrationen – nicht wissen, woher ihr Input kommt und wohin ihr Output fließt. Sie sollen ihre Umgebung nicht beachten. Ich nenne das malthe Rule of Mutual Oblivion (RoMO).

Bei Anwendung dieser Regeln entstehen Funktionsbäume, die speziell sind und sich sehr von denen unterscheiden, die Sie in Ihrer Codebasis finden. In diesen Bäumen existiert Logik nämlich nur in den Blättern – während sie üblicherweise über die ganze Tiefe von Funktionsbäumen verschmiert ist.

Logik in den Blättern hat nun jedoch einen entscheidenden Vorteil: sie ist wunderbar einfach testbar. Sie hängt ja nicht mehr von anderer Logik ab. Die Blätter sind Operationen, darüber liegen potenziell viele Strata von Integrationen.

Das bedeutet, es sind keine Testattrappen mehr nötig, um Logik zu überprüfen. Und das wiederum führt dazu, dass Overhead durch Dependency Inversion und Dependency Injection reduziert wird.

Aber Achtung: Es geht nur um Testbarkeit. Alle Operationen sollten zwar testgestützt implementiert werden. Doch nach Fertigstellung sollten diese Tests in den Papierkorb wandern, sofern die Operationen keine öffentlichen Produktionsfunktionen sind. Die Tests sind lediglich Gerüste für eine korrekte Implementation im Kleinen und somit temporär. Auf lange Sicht wird die Korrektheit durch Regressions- und Korrektheitstests zugesichert, die bis in die Operationen wirken. Zusätzliche Tests würden die Wandelbarkeit einschränken. Das bedeutet auch, dass private Operationen privat bleiben sollen, selbst wenn sie kurzzeitig für Gerüsttests öffentlich gemacht werden müssen.

Wie gesagt: Es geht bei der Wandelbarkeit nur um Testbarkeit, nicht um dauerhafte Existenz von Tests. Bei Bedarf soll es einfach sein, Tests für Logik-Aspekte jeder Granularität aufzusetzen.

Grundsätzlich sehr gute Testbarkeit ergibt sich durch Befolgung der Regeln IOSR und RoMO. Sie sorgen dafür, dass Produktionsfunktionen ganz von allein vergleichsweise klein bleiben. Die Logik in Operationen ist viel fokussierter als in üblichen Codebasen, weil IOSR verhindert, dass Request/Response-Aufrufe zwischengeschaltet werden.

Dennoch können Operationen „schlecht geschnitten“ sein oder mehrere Verantwortlichkeiten enthalten. Das zu beurteilen, ist jedoch aufgrund der Form kaum möglich. Solch inhaltlichen Aspekte sind Sache des domänenkundigen Betrachters.

Aber es gibt weitere Indizien für gute Testbarkeit:

  • Zustandslosigkeit
  • Unabhängigkeit von externen Ressourcen, z.B. Datenbank, TCP-Verbindung, UI-Framework

Das bedeutet im Umkehrschluss: Abhängigkeiten von Zustand bzw. externen Ressourcen, die zentral für jede Software sind und sich nicht wegdiskutieren lassen, sollten wenigstens in eigenen Produktionsfunktionen gebündelt werden.

Nicht nur führt IOSR also zu einer vertikalen Strukturierung und RoMO zu einer horizontalen Entkopplung einhergehend mit kleinen Funktionen, es werden auch noch weitere Funktionen „ausgetrieben“ durch Trennung grundsätzlicher formaler Aspekte. Ich nenne das mal the Rule of Separating Concerns (RoSC).

Strukturen abstrahieren

Bisher habe ich nur von Produktionscodefunktionen gesprochen. Was ist aber mit Klassen und den darüber liegenden Modulebenen? Sie entstehen für mich durch rigorose Abstraktion.

Wenn Funktionen die ersten Strukturelemente sind, um auf die Kräfte Korrektheit, Regressionssicherheit und Wandelbarkeit zu reagieren, dann entsteht eine Menge, in der sich Muster erkennen lassen. Diese Muster sind für mich die Grundlage für die Zusammenfassung von Funktionen in Klassen. Klassen abstrahieren von den konkreten Funktionen; sie geben Mustern einen Namen.

Hier werden ein paar Funktionen zusammengefasst, die mit den selben Daten arbeiten, dort einige, die den selben API nutzen, da drüben welche, die sich um den selben Domänenaspekt kümmern usw. usf.

Das ist für mich „Objektorientierung von unten“. Es stehen nicht Klassen und Objekte am Anfang, sondern Funktionen. Softwarestruktur wird damit durch Verhalten getrieben und nicht durch Daten.

***

Clean Code ist notwendig. Je länger eine Codebasis leben soll, desto wichtiger wird es, nachhaltig mit ihr umzugehen. Das Gefühl muss über die dominanten Kräfte Funktionalität und Effizienz hinaus reichen; Sensibilität für Korrektheit, Regressionssicherheit und Wandelbarkeit muss entstehen.

Doch diese Kräfte sind immer schwächer als Funktionalität und Effizienz. Wer das kompensieren will, wer eine differenziertere, eine zukunftsfähigere Balance herstellen will, der braucht hard-and-fast rules, die auch mit Tunnelblick unter Stress noch angewandt werden können. Das Resultat muss dann nicht perfekter Clean Code sein, aber deutlich besserer als heute.

Mit den vorgestellten Regeln ist das aus meiner Sicht möglich. Auch die brauchen noch Disziplin und Übung, doch sie sind viel, viel klarer als die ewig zitierten Prinzipien des Clean Code.

Wenn ich meine Empfehlung zur Reaktion auf die Kräfte Korrektheit und Regressionssicherheit noch zusammenfasse zu Test-first All Contracts Rule (TfACR), dann lautet die Zusammenfassung:

  1. TfACR
  2. IOSR
  3. RoMO
  4. RoSC

Durch diese Regeln, so glaube ich, entstehen Softwaresysteme, die eine deutlich saubere Grundstruktur haben.

Und jetzt: Nicht lang schnacken, Kopf in Nacken :-) Wie wir in Norddeutschland sagen. Machen, statt reden. Engage!

A Different Perspective on a TDD Lesson – Terrain Generation, Part III: Implementation

Implementation follows design. Finally I can sling some code ;-) But where to start? Analysis and design left me with some options. There is not just an Interpolate() method like Robert C. Martin had, but several classes and methods.

On the surface of the system under development there are:

  • TerrainGenerator.Interpolate()
  • Terrain.Size
  • Terrain.this[]
  • Terrain.ToArray()

And below the surface I found:

  • ShapeHierarchy.Shapes
  • Calculator.Process

Interpolate() will be simple to do once all other modules are coded. It’s the integrating function, it represents the whole of the algorithm to implement. I’ll do it last.

Implementing Terrain

The analysis already provided some test for Interpolate(). So since Terrain does not depend on other classes, it shall be the first module to code.

The tests are straightforward. I write them both down:

No stepwise testing development needed as TDD advocates, because the implementation is trivial:

Terrain is almost a pure data structure, very little logic is needed. The interesting stuff is one line to calculate the size of the square and a couple of lines to initialize the vertexes.



Implementing Calculator

Calculator seems to be the next easiest class. Just one public method (although a user of the solution won’t see it).

But Calculator.Process() consist of two aspects: averaging and jitter. I take a TDD approach and start with just calculating an average:

The implementation is trivial using C# Linq:

Now on to adding some randomness:

Since the average does not need to be checked again I just pass in 0.0f as a single value. That way the random value (based on offset and amplitude) will be the result of Process().

Using 1.0f and -1.0f as „random“ values tells me if the amplitude is able to increase and decrease the offset:

I could have put adding the jitter into a method of its own. But since Process() is only two lines in total and focussing on the jitter by choosing certain input values is very easy, I chose not to add another module (yet).

Implementing ShapeHierarchy

Implementing the shape hierarchy is the most challenging part of the whole thing. It’s the heart of the algorithm. I’ve to be careful to get it right. Small steps seem prudent.

First two „acceptance tests“, i.e. tests of the overall functionality. Once they become green I deem the implementation of ShapeHierarchy.Shapes done.

I need to check if shapes of the correct number and with the correct center and vertexes get produced for certain sizes. It’s not important if those shapes represent diamonds or squares. Averages are calculated just from vertex values on a terrain.

To make comparison easy I use the ToString() function of the Shape and Coordinate data structures to get shapes serialized. That way I just need to check string equality and don’t need to implement Equal() methods ;-) An added benefit: whenever something should go wrong with shapes and I need to debug, the IDE will readily show me shape contents as nicely formatted strings.

Of course I need to do the calculation of the expected shape coordinates by hand :-( But the acceptance tests I described earlier help:

I encoded the level of the shapes in their cell color (level 1: dark blue, light blue, level 2: dark green, light green) and chose a certain order for their calculation (squares will start with the north-west vertex, diamonds with the north vertex).

But what now? How to proceed with the implementation?

Some more design

I don’t like to drive an implementation just through the bottleneck of a single function. That might be ok for some very small functions like Calculator.Process(). But this here is more complicated.

Instead I insert another small design phase. Let me see if I can derive some more structure from the requirements for this specific method.

From what I can see in the specs there are squares and diamonds on each level. For a 3×3 terrain it’s one square and 4 diamonds on just one level where their radius is 1 (2^(n-1)). For a 5×5 terrain it’s one square and 4 diamonds on the first level with radius 2, and then 4 squares and 12 diamonds on a second level with radius 1.

There are levels with decreasing radius starting from 2^(n-1) each containing squares and diamonds which are independent of each other. Instead of a big problem there are now two small problems – and the big problem repeated on a lower level. Recursion to the rescue!

What I need are 3 functions:

  • Creating all shapes on a certain level with the appropriate radius – and in the end calling itself again for the next lower level.
  • Creating all squares with a certain radius.
  • Creating all diamonds with a certain radius.

The recursive function will be called by the Shapes property. And it’s easy to do: just concat the streams of squares and diamonds on one level with the stream of shapes from the next lower level.

There are tests for Shapes, but no tests for Enumerate_shapes() are needed. It’s a small plain integration function. I’ll know from the tests on Shapes if it’s correctly wiring together its functions.

The real workhorses are IEnumerable<Shape> Enumerate_squares(int size, int r)and IEnumerable<Shape> Enumerate_diamonds(int size, int r). Both take a radius r to generate shapes of the correct size for the level, and the size of the overall terrain to not include coordinates beyond its boundaries.

This is no rocket science. I really just got that from carefully studying the specs. The images there are telling this story. (And don’t get confused with „diamond step“ and „square step“. The diamond step actually calculates the center value of a square, and the square step calculates the center values of diamonds. At least that’s what I see in those images.)

Implementing private functions

The newly found shape enumeration functions are small, but a little bit tricky. I need to get the center point calculation right. It’s simple for the only square on the first level:

But how about the 16 squares on the third level of a 9×9 terrain? It’s too many for me to check all of them. So I’m doing just a spot check:

Here’s the implementation. Creating a shape at x/y with a certain radius is easy. I just need to be careful to calculate the vertexes in the order I expect in the tests. What’s a bit tricky is to vary x and y across the terrain on a level: where to start, how much distance between the center points?

But again with careful analysis of the specs it’s pretty straightforward ;-) And the same is true for diamonds, although they are generated with two sequential loops.

Integration

With all the details implemented and tested I can move on to integrate them. That’s the sole purpose of Interpolate(). It does not do anything on it’s own; it does not contain logic. It just pulls together all the parts and welds them together into a whole.

That’s no rocket science. All shapes come in the right order from the ShapeHierarchy; their vertex values get collected from the Terrain; the center value is calculated from those values. That’s it. Very straightforward.

And it makes the original acceptance tests go green! Success! :-)

Manual acceptance tests

The automatic tests tell me the implementation is finished. Problem solved. But can I trust them? A green test of 25 floating point values is not very tangible. For me at least. Also the tests for the shape generation might be green – but what does that mean for realistically sized terrains? I’m unable to write tests for 1025×1025 terrains.

Enter the test station. Now I can run it to put terrain generation to a real test. Will I be visually pleased by the calculated terrains?

See for yourself:

I think this terrain (and others) is looking pretty good. And if not, I guess it’s more the fault of my kind of visualization than of the algorithm’s implementation.

Removing scaffolding tests

Since I did not progress according to the TDD steps but did explicit design there’s no need for refactoring. The solution is already in good, clean shape. At least to my taste.

Nevertheless I will do some clean up. I will remove tests.

Yes, I think there are too many tests. All tests of methods which should not be visible on the surface should be deleted. They become apparent when I set the visibility of methods like Enumerate_Squares() to private.

Tests working on those methods were useful while building the solution. Like a scaffold is useful while building a house. But once the house is finished, the scaffold is removed. That’s why I remove those tests. They would be white box tests and make the test suite brittle.

The only tests I keep are those of public methods. They are my bulwark against regressions should I need to change the codebase. That of course requires them to be comprehensive so that enough code paths are covered.

Summary

That completes my journey through the terrains of the Diamond-Square algorithm. It was fun. A different kind of problem than I usually tackle.

I hope you found it interesting to watch the solution unfold along a different path than TDD would suggest. TDD to me has value and it’s place – but I also think it needs to be padded by comprehensive analysis and explicit design. Neither code nor design should be a matter of surprise or afterthought. „Think before coding“ is the way I suggest for truly clean code.


A Different Perspective on a TDD Lesson – Terrain Generation, Part II: Design

Analysis drives the structure of the solution for the Diamond-Square algorithm from the outside. It defines a syntactic – and semantic – overall contract for the whole. It thus already shapes the code without much creative effort from my side. And it produces criteria to assess if the desired behavior has been achieved.

Before I start coding, though, I like to look deeper. Is there more I can glean from the specs in terms of solution structure? Are there special terms or structures or features which are characteristic to the problem domain and thus should be made explicit in code?

I believe you should manifest insights, aspects, particular problems, noteworthy decisions very clearly in code. Make them public and easy to see for everybody (I mean yourself and your fellow developers). They should not be swept under the rug as implementation details.

So far that thinking has lead to the Terrain class and visible random number generation. The algorithm is not just about behavior. It’s equally about a data structure, and a special one for that matter. And since the use of random numbers make the core function Interpolate() hard to test, they need to be made explicit, too.

Data structures

But are there further notable structures spoken about in the specs? Yes, I think so. They are even right in the title. There are squares and there are diamonds. That’s geometrical shapes. Look here:

The square is defined by its center (orange) and its four vertexes (black). The same goes for a diamond (4 of them in this picture):

This, to me, begs for representation in code. If I was presented with code solving this problem, I’d look for such kind of structures. At least a Shape class seems in order, e.g.

Whether there should also be subclasses Square : Shape and Diamond : Shape, I doubt. The difference between a square and a diamond is not in it’s basic structure (center, 4 vertexes), but how the structure is filled (see below).

Also, what I read in the specs is that there is a hierarchy. Shapes don’t stand alone, but are nested. Can you see that, too?

In a 5×5 matrix like depicted in the Wikipedia article I see two nested levels.

On each level there are squares and diamonds with a certain „radius“. The radius starts with 2^(n-1) and is divided by 2 with each level (for a matrix with a size of 2^n+1). Squares are nested within squares, diamonds are nested within diamonds, if you will.

For me this should be represented in code with a dedicated data structure, e.g. ShapeHierarchy. Its purpose is to represent all shapes a terrain is made up of so I can iterate over them. I deem that a responsibility of its own which should be encapsulated in a dedicated module.

Robert C. Martin, too, thinks the shapes or at least their coordinates are special:

I want to know that the coordinates of the midpoint of the square are being calculated properly. I also want to ensure that the value of the midpoint is set by taking the average of the four corners. So I’m very interested in the calculation of the coordinates of those four corners.

However he does not derive from that the need for an explicit representation which can be tested individually. Instead he’s testing coordinate generation through the bottleneck of his algorithm’s root function interpolate() by letting a clever contraption accumulate coordinates in a string, e.g.

I prefer to make things more explicit. If there’s an important concept hidden in a problem it should be represented by a module (function, class, library etc.). That’s a form of documentation of my understanding and avoids the accumulation of concepts into a monolith.

Shapes and the shape hierarchy are no detail of my solution. Sure, the user does not need to know about them. But any fellow developers should. And since considerable testing is needed to get these aspects right, that should not require any test wizardry.

So much for structures I’m able to glean from the problem description. But what about the functionality?

Behavior

The overall functionality is to fill a matrix with values. That’s what Interpolate() represents to the user. But how does interpolation work? Is it a process with distinct steps or different aspects? Sure it is. Not much creativity is needed to come up with at least two.

  1. The vertex coordinates for each square and diamond need to be determined. That’s what is represented by the shapes and the shape hierarchy.
  2. And then for each shape the center cell’s value has to be calculated.

I don’t need TDD to stumble across this. It’s there in the algorithm’s description. So why not represent it explicitly in the code right from the start? Both aspects are not surprising details but should lead to consciously designed modules. No refactoring needed to arrive at them. They can easily be tested separately.

The ShapeHierarchy with its Shapes property stands for the first aspect. The second can be encapsulated in a Calculator, e.g.

But wait, what does calculation mean? It’s averaging the values and then adding some random jitter to the result. It’s this transformation which depends on the parameters passed to Interpolate() for creating randomness. That should show in the definition of Calculator:

Summary

Analysis is about understanding a problem. Understanding is represented by examples and modules which describe a „surface“, the contract. Then the examples get translated into tests for these modules.

Design is about finding a solution to the problem based on the understanding. A solution is described by internal modules representing behavior and data. A solution does not contain any logic. It’s declarative. It tells what needs to be done, not how.

The solution design is like a map. It’s rough, but still it’s useful to navigate through reality.

For the Diamond-Square algorithm the solution is

Break the terrain up into a hierarchy of squares and diamonds (shapes). For each shape calculate the value of its center from its vertexes. Fill the terrain with these values.

This is a declarative solution design given in prose. And I think it’s good clean code practice to represent its main problem domain concepts like shape, hierarchy, calculate, center, vertex, terrain in code as distinct modules. To wait for them to appear by doing TDD is to waste time and to make testing harder than needed.


A Different Perspective on a TDD Lesson – Terrain Generation, Part I: Analysis

How to do TDD on a not so simple problem? Robert C. Martin has published a lesson on that tackling the Diamon-Square algorithm for terrain generation.

I think that’s a really interesting problem to consider as an exercise in software development to sharpen your skills. Today I presented it to a Clean Code training group and we had a lot of fun for a couple of hours.

Unfortunately I do not agree with how Robert C. Martin approached the solution. For one I find his statement

Don’t worry, it’s a very simple algorithm; and the linked article is extremely easy to read.

a bit misleading. It sounds as if this problem was on par with the usual code katas like „FizzBuzz“ or „WordWrap“. But in my experience it is not. It requires far more time to solve. Don’t try to cover it in a 90 minutes coding dojo with your programmer pals. You’ll give up frustrated.

But don’t worry. I won’t bother you with all the other reasons I don’t concur with Robert C. Martin. Instead let me just demonstrate a different perspective. If you find this an interesting contrast I will have accomplished what I want :-)

Analysis

Before you can code anything, you need to understand the problem. Understanding is the result of analysis. So the first step on my way to solving the Diamond-Square problem is to analyze the requirements, to look very closely. Whatever is already there can help me make coding easier. Any misunderstanding will create detours and waste.

Unfortunately there’s no product owner to explain the requirements to me and answer my questions. I’ve to drill my own way through the „specification“ given by Wikipedia.

Question no. 1 to be answered: How does the user of my solution want to use it? What’s the formal or syntactic contract supposed to look like?

Question no. 2 is: How does correct behavior of the System Under Development (SUD) look like? Is there a usage example?

In my view those two questions need to be answered in an unambiguous way. And the answers need to be written down. They even need to manifest in code. That’s also part of clean code.

Syntactic contract

How should the solution look on the outside? Let’s check what the specs are giving away:

The whole thing is revolving around a matrix, even a square matrix. A matrix of floating point values is created. The simplest way to express this in code would be a 2D array, e.g. float[,].

But not just any float array will do. First it has to be a square, then only certain sizes are allowed for the square; the length of a square – I call it its size – must be 2^n+1, e.g. 3, 5, 9, 257, but not 4 or 7 or 128. How can such a 2D array be enforced? How about a special type which always will lead to matrixes created correctly? It could pretty much look like a 2D float array, but a more constrained one. Here’s a simple usage example of such a type:

When creating such a matrix the user passes in n, not the size. That way the matrix can calculate the proper size.

Also I read that matrixes to be filled by the algorithm always start out with the corner cells set to initial values. Why not make that clear by adding constructor parameters?

In the end of course the terrain should be available as a plain float array for further processing by other code. The dependency on the special array-like type Terrain should be limited. A user needs a way to turn the matrix into an ordinary float array:

So much about the basic data structure the algorithm revolves around. But how to fill it with terrain height data? That sure is not a responsibility to be carried by the data type Terrain. Its sole purpose is keeping data in a consistent way.

Another module is needed. One that does all the necessary processing. An initialized Terrain will be its input. And in the end this Terrain is filled according to the algorithmic rules. A static method will do, I guess:

The values of the corner cells won’t be changed by the interpolation. The above test thus will be green. But what about the other cells. Their values will be calculated, that’s what the algorithm is about. It states that each cell’s value is the average of the values of some „surrounding“ cells plus a random value. For the above 3×3 terrain this would mean for the center cell t[1,1]: (1.0 + 2.0 + 3.0 + 4.0) / 4 + randomValue or 2.5 + randomValue.

To check for a correct calculation thus is not as simple as Assert.AreEqual(2.5f, t[1,1]). It probably would fail mostly because I cannot expect the random value to be 0.0 all the time. That would be against the definition of randomness ;-)


GFU Clean Code Powerdays

Semantic contract

So far the contract definition pretty much is about the syntax of the solution, its static surface: which classes should there be with which functions.

The contract defines how the solution can be used, e.g.

Nothing much can go wrong now. This makes very explicit what the requirements describe.

But what’s the behavior supposed to look like? t is only input to Interpolate(). What’s the output? To expect the corners to be initialized is not much of an expectation. That’s trivial and hardly needs testing. I’m talking about all the other cell values in a Terrain matrix.

As explained above they get calculated. But Interpolate() is not a pure function. Calling it multiple times with the same input won’t produce the same output due to the involvement of randomness.

That, too, is an aspect of the solution that needs to be explicitly expressed, I’d say. Without making the influence of randomness visible to the outside the solution will not be easily testable.

The least would be to inject into the solution a source of random values. Then it can be replaced for testing purposes if needed. But what’s the range the random values should be in? Wikipedia does not make any statement on this. But Robert C. Martin suggests two more values: an offset and an amplitude. If I understand him correctly this would mean a cell’s value should be calculated like this:

Or to further separate the aspects:

Setting offset and amplitude to 0.0 would switch off random variations of averages.

To me these additions make sense. If a terrain is supposed to be made up of values in a certain range, then random values cannot just be between 0 and 1 as they usually are provided by random number generators. They need to be in the ball park of the initial values or whatever the user wants.

This leads to another modification of the contract:

Now the user can control the randomness or the „jitter“ of the interpolated values. This is great for testing purposes! And it makes an essential part of the algorithm obvious.

For convenience sake, though, an overloaded function can be defined which does not take a function for random value creation but provides a default internally. That’s for production use:

But during tests the parameter injection will come in handy. It will be easier to provide examples for the semantic contract.

The semantic contract is about input-output relations. Which input leads to which output? Possibly some resources also might be involved like in-memory data or a file or the printer. The overall change of data during transformation is what’s perceived by the user as the behavior of the software:

Or with the resource state extracted from the transformation and added to the data flow:

transform() is stateful, transform'() is stateless, i.e. a pure function.

Semantic contracts define the input data plus the resource state before the transformation and the output data plus the resource state after the transformation.

Are there any examples of Diamond-Square behavior in the specification? Unfortunately not. I have to come up with sample inputs and outputs myself, it seems.

No, wait, Robert C. Martin provided an example. For a 5×5 matrix with all corner cells set to 0.0 as the initial value, an offset of 4.0 and an amplitude of 2.0, plus all random values being 1.0.

[This] simply turns off the randomness, and uses the absolute values of the random amplitude instead.

The effect of this is that 4+2*1=6 will be added to all averages calculated. The resulting terrain looks like this in his test code:

Great! Thanks for doing all the calculations. It’s tedious.

But, alas, this does not prove anything about my own understanding. I should come up with an example myself. Bummer :-(

I could pick up pen and paper and do the calculations in my head – but why not put Excel to some good use? Here’s my first example, just plain averages (no offset, no amplitude, no randomness):

Still though, I don’t know whether that’s correct. I just carefully tried to translate the calculation rules from the algorithm description into Excel formulas. See how each cell refers to other cells to pick the values to average (and then add the random jitter):

This is one of the challenges of this problem: to come up with correct examples.

But now that I have set up Excel, why not vary the data a bit? Here are the expected results for some other initial data and jitter:

And now that I’m at it, why not start with the same data as Robert C. Martin?

Hm… strange… my results look different from his. Why’s that? Take cell (1,1) for example: his value is 8.5, mine is 11,5. According to the specs the source cells to pull the values from for the average are these:

In Robert C. Martin’s matrix those values are 0.0, 8.0, 6.0, 8.0 with an average of 5.5. If I add the constant jitter of 6 to this I arrive at 11,5, but his value is 8.5.

Strange. Who’s wrong, who’s right? Without knowing how he did it, I don’t know. I believe I applied the rules correctly.

Anyway, that’s a couple of examples I can start with. I should translate them into automated acceptance tests. Here’s how they look:

They are simple, but typing in the expected matrix values is no fun :-( You’ve to be very careful to not introduce subtle mistakes.

Assert_matrix_equality() is a small helper method to compare the floating point values of the cells using a delta. The test framework’s Assert.AreEqual() does not allow for that when throwing arrays at it.

A test station for manual tests

Analysis could be complete now. I guess I understand how to calculate terrains using the Diamond-Square algorithm. I even can generate all kinds of examples for automated tests. They only suffer from one flaw: they are small. Setting up an Excel sheet for matrixes larger than 5×5 is prohibitive. So what about examples of size 9×9 or even 1025×1025?

There won’t be any. Sorry. That’s one of the reasons Robert C. Martin picked the algorithm for his TDD lesson. You cannot exhaustively check the result of the solution using examples. You need to find another way to gain confidence in your code. But that’s a matter of the phases after analysis.

So how could I check the correctness or rather „fitness“ of larger terrains? There’s no way I can do that precisely, i.e. by cell value, but I can at least check the overall visual impression of a terrain. In the end terrains are generated to be looked at, anyway. I don’t care for values but for visual appeal :-) Do the terrains look realistic or at least interesting?

To easily check that I set up a special test station for the algorithm. It generates terrains based on manually entered input values and shows them as a bitmap:

For the transformation of a floating point array into a bitmap I have to create a small mapper (Heatmap), though. It translates the terrain data into rainbow colors. But the effort for this is compensated by the ease with which I can check terrain generations at any time.

Get_input() reads some data from the console. The second parameter is the default value in case the user just presses ENTER.

Setting up such a test station might look like too much for such a small problem. But at least I would really like to see the terrains that get produced, not just floating point arrays. Since I don’t know graphics frameworks to render float arrays, I’m doing the translation myself.

Summary

Analysis is not just a mental exercise. Its results should be documented – as tests. Examples plus function signatures are concrete evidence of understanding. They should be manifested in code.

Such acceptance tests do not hint at any implementation, though. They don’t need to be incremental in any way. Hence you can expect them to stay red for quite a while.

But as soon as they turn green, you know you’re done :-)

In addition a test station for manual tests provides a means to make the solution more tangible in isolation. With it tests can be run by anybody.

However, doing the analysis in this way does not only result in tests which help stave off regressions. It helps structuring the solution. The specs don’t talk about functions or classes. But careful reading provides information on what should be tested. And that leads to a certain structure. Different aspects get expressed differently: a data structure with certain properties becomes a class, behavior of some kind becomes a function, dependencies on resources are made explicit.

To me that’s clean coding right from the start.


Was Führungskräfte lesen (2017)

Der Jahresanfang ist ein guter Moment für die Orientierung. In welcher Richtung liegt doch gleich Ihr Ziel? Was wollten Sie erreichen? Haben Sie sich überhaupt etwas vorgenommen?

Wenn Sie für 2017 Clean Code Development auf den Zettel schreiben, freuen wir uns natürlich besonders. Aber auch wenn nicht, wünschen wir viel Erfolg!

Und wir können sehr empfehlen: Nehmen Sie sich auch die Lektüre des einen oder anderen Buches vor. Lesen bildet, heißt es doch ;-) Allemal kann Lesen sehr kostengünstig einen Blick über den Tellerrand bieten. Da gibt es bestimmt einiges zu entdecken, das Sie anregt oder motiviert oder ins Grübeln bringt. Oder es verschafft Ihnen einfach nur Entspannung.

Wir haben deshalb mal einige Führungskräfte aus dem Bereich Softwareentwicklung gebeten, uns und Ihnen zu verraten, was sie denn gerade lesen oder welche Bücher sie im vergangenen Jahr beeindruckt haben.

Vielleicht ist da ja auch etwas für Sie dabei. Das würde uns freuen. Viel Spaß beim Stöbern.

Leseempfehlungen

Johannes Boppre, Entwicklungsleiter, Corporate Planning AG, Hamburg

Als Wahlpfälzer, Weinliebhaber und notorischer Anekdotenerzähler ein absolutes Muss für mich und zudem immer wieder unterhaltsam zu lesen. Absolute Leseempfehlung für alle Leute, die gerne mal einen Zeh in die verrückte Welt der Weinliebhaberei tauchen wollen.

Sehr unterhaltsames Buch über einen Mann, der sich mit den Supersportlern im Gedächtnistraining austauscht und dann selbst einer wird. Man lernt sehr viel über die Funktion und Möglichkeiten des eigenen Gehirns. Die Erkenntnis, warum uns mit zunehmendem Alter die Jahre subjektiv immer kürzer vorkommen und was wir selbst daran ändern können, war für mich sehr schön.

Super Unterhaltsame Bücher von Mark-Uwe Kling, humorvoll und sozialkritisch, ich habe selten während meinen Autofahrten so viel gelacht wie bei diesen Hörbüchern.



 

Uwe Henker, Bereichsleiter Softwareproduktion, medatixx GmbH & Co. KG, Bamberg

Das Buch lese ich noch. Bin gespannt und ängstlich. Ein Glück, dass ich schon in 17 Jahren in Rente gehe…

Das Buch habe ich wegen der Normalen gelesen. War interessant, aber meine Sicht auf die Normalen ist natürlich nicht besser geworden. Ich hoffe nur, dass ich nicht normal bin und es auch nicht werde.

Ich fand es super spannend, aber auch beängstigend.

Das kam natürlich nach dem Buch von Keese. War interessant aber auch erschreckend. Dann hatte ich Menschheit 2.0 begonnen, aber nur wenig gelesen. Habe jetzt aber wieder angefangen.


 

Guido Grogger, Chapterlead/Teamlead Java, Interhyp AG, Berlin

Wer noch glaubt, dass der Mensch ein Wesen ist, welches möglichst rationale Entscheidungen trifft und dabei ausschließlich die eigene Nutzenmaximierung anstrebt, dem sei das Buch von Verhaltensökonom Dan Ariely ans Herz gelegt. Mit Hilfe der Ergebnisse vieler Experimente aus der Verhaltensforschung stellt er den „Homo oeconomicus“ stark in Frage, welcher immer noch trotz aller Kritik eine tragende Rolle in der Wirtschaftstheorie spielt.

Eigentlich war das Aussortieren für mich die unangenehmste Aufgabe bei Aufräumen. Nach der Lektüre dieses Buches mache ich es nun mit Freude und Leichtigkeit. Marie Kondo hat eine Methode entworfen, bei der es darauf ankommt, beim Sortieren den Verstand auszuschalten und statt dessen auf das Glücksgefühl zu hören. Klingt vielleicht seltsam, funktioniert aber wunderbar.

Dieses Buch gibt mir in unserer krisen- und katastrophengeplagten Zeit Hoffnung auf eine bessere Zukunft. Ganz im agilen Sinn wird dafür kein Masterplan entworfen, der einfach in der richtigen Reihenfolge und zur richtigen Zeit umgesetzt werden müsste. Statt dessen werden Wege gezeigt, wie jeder Einzelne mehr Verantwortung für die Gesellschaft übernehmen kann. So können wir uns langsam aber sicher durch viele Ideen und Experimente an eine nachhaltige und lebenswerte Zukunft herantasten.


 

Frank Lindecke, Entwicklungsleiter, ma design GmbH & Co. KG, Kiel

Seit 2013 hat mich das Fieber der Fotografie gepackt. Bei den Bildern geht es für mich um Schönheit im Aufbau, den Orten, den Emotionen, die diese Bilder in mir erzeugen. Zum Lernen braucht es Imitation. Davon bin ich überzeugt.

Die Verbindung zur Software Entwicklung ist, dass ich mir öfter mal „fremden“ Code anschaue, um Inspiration zu bekommen. Den Stil des Entwicklers zu erleben und seine Ansätze zu „schmecken“.

Das ist ein sehr persönliches Buch und ich habe wirklich überlegt, ob ich es in diese Liste packe. Da es aber bei mir liegt, gehört es dazu.

Das besondere an diesem Buch für mich ist, dass es so viele Wirklichkeiten von Männlichkeit gibt und diese sich immer wieder wandelt. Wir können immer wieder etwas lernen für uns und manchmal braucht es dazu Tränen oder ein Lachen.

Verbindung zu Software? Weiß ich nicht. Außer vielleicht, dass es viele Wirklichkeiten geben kann, etwas zu meistern. Und sei es das Größte, unser Leben.

Das Buch hat mich schon immer interessiert und ich hab es mir im Herbst bestellt und in den Weihnachtsfeiertagen reingeschnuppert.

Ich kann es verstehen, wenn man „dem Kampf und Kriegsführung“ skeptisch gegenüber steht. Spannend fand ich das Kapitel 8 „Die neun Anpassungen“. Da wird von Folgendem geschrieben „Für einen General gibt es fünf Gefahren. Ist er todesmutig, kann er leicht getötet werden. Hängt er zu sehr am Leben, ist er leicht gefangen zu nehmen. „.

Es geht für mich dort eben gerade nicht um ein „Entweder-Oder“, sondern eher um ein „Sowohl, als auch“.

Meine Idee ist es, dies in Software Entwicklung zu übersetzen. Ich glaube, dass es auch dort viele Parallelen gibt, die wir dort beherzigen können und sei es einfach in dem Umgang mit uns selbst und unseren Kollegen und Kunden.


 

Alexander Schütz, Entwicklungsleiter, medatixx GmbH & Co. KG, Bamberg

Schluss mit defensiver Programmierung

Ein Muster von Trainingsteilnehmern, das ich häufig sehe, wenn wir die Resultate von Übungsaufgaben anschauen, ist das defensive Programmieren. Hier ein Beispiel:

Die Übungsaufgabe war die Function Kata fromRoman; es galt also, römische Zahlen in Dezimalzahlen zu konvertieren. Die zu schreibende Funktion lautete int fromRoman(string roman). In einer ersten Iteration sollten nur einziffrige römische Zahlen konvertiert werden, z.B. „X“ oder „D“.

Der Code, über den ich dann gestolpert bin, sah ungefähr so aus:

Zweierlei hat mich daran gestört, sogar sehr gestört.

  1. Es wurde vom Kontrakt abgewichen. Die vom „Kunden“ gewünschte Signatur sah int als Ergebnistyp für die Funktion vor, nicht int?. Ob das eine gute Entscheidung des „Kunden“ war, wäre zu diskutieren gewesen. Doch diese Diskussion fand nicht statt. Also war es die Aufgabe der Trainingsteilnehmer, die Funktion ohne nullable type umzusetzen.
  2. Im Code wurde eine Validation vorgenommen. Die Funktion prüft, ob überhaupt eine römische Zahl anliegt. Wenn nicht, reagiert sie ganz bewusst speziell. (Dass sie null zurückgibt, statt eine Exception auszulösen, ist eine Folge der Entscheidung für einen geänderten Ergebnistyp.)

Zu meiner Schande muss ich gestehen, dass ich in der Situation nicht ganz trainerprofessionell reagiert habe :-/ Ich war nicht ruhig, verständnisvoll, besonnen, sondern in meiner Verzweiflung etwas polterig. Leider. Sorry!

Vielleicht hat das ja nun aber auch etwas Gutes. Ich schäme mich und versuche auch auf diesem Weg eines Blogartikels, ein wenig Buße zu tun. Geschieht mir recht, nochmal Zeit zu investieren, um meinen Standpunkt klar zu machen. Jetzt in Ruhe.

Also, was hat mich denn so verzweifeln lassen? Meine Reaktion hatte ja nichts mit dem armen Teilnehmer zu tun, sondern nur mit mir selbst. In mir hatte sich etwas angestaut. Frust angesichts meiner Ohnmacht, dieses unselige Muster aus der Welt zu schaffen. Denn Stefan Lieser und ich sehen es wirklich in jeder Trainingsgruppe. Immer und immer und immer wieder fügen Entwickler dem Code, der unzweifelhaft geschrieben werden muss, weiteren hinzu. Die Begründung: „Das habe ich so gelernt. Das ist defensive programming.“

Was soll ich sagen…? Das macht mich sprachlos. Vielleicht ist es die Tragik, die dieses Gefühl noch verstärkt. Denn Entwickler wollen ja das Gute, sie bemühen sich um Qualität – und erreichen das Gegenteil. Sie optimieren lokal und temporär – indem sie im Moment des Codierens eine Qualitätssicherungsmaßnahme ergreifen –, doch verfehlen sie dadurch ein globales, langfristiges Qualitätsoptimum. Defensive Programmierung ist keine Clean Code Entwicklung!

Ok, was habe ich denn für ein Problem? Warum hat mich der obige Code lospoltern lassen? Weil defensives Programmieren das Geld des Kunden verschwendet.



In diesem Beispiel ist das ganz deutlich: Der „Kunde“ hat sich nicht gewünscht, dass eine Validation stattfindet. Das ist erstens an der beauftragten Funktionssignatur abzulesen; die enthält keinen nullable type. Das ist zweitens an der Liste der Testfälle abzulesen, die für die Funktion definiert waren. Es gab keinen Test, der die Übergabe von null oder leerer Zeichenkette (oder überhaupt irgendeiner ungültigen römischen Zahl) durchgespielt hätte. Das ganze Thema „ungültige Eingaben“ war sogar aus-drück-lich ausgeklammert.

Dennoch sehen sich Entwickler immer wieder bemüßigt, Zusatzaufwand zu treiben, um solche Fälle abzudecken. Sie ignorieren Anforderungen oder sie imaginieren sie. Ich weiß nicht.

Was ist der Antrieb dahinter? Geht es wirklich darum, dem Kunden mehr Qualität zu liefern? Ich glaube nicht. Eher vermute ich den-eigenen-Arsch-absichern als Beweggrund. „Soll keiner sagen, meine Funktion hätte aufgrund falscher Eingaben Müll produziert!“ oder so mag der Gedanke sein. Nicht Altruismus, sondern Egoismus treibt Entwickler, das Geld des Kunden in defensives Programmieren zu versenken. Sie selbst wollen sich Ärger in der Zukunft ersparen.

Das finde ich verständlich; sie haben bestimmt einige Negativerfahrungen gemacht, die sie zukünftig vermeiden wollen. Doch das Mittel ist aus meiner Sicht gänzlich falsch gewählt.

Noch schlimmer wird das bei defensiver Programmierung wie dieser:

Ja, das hat tatsächlich jemand empfohlen. Sehen Sie das Problem? Gruselig, oder? Was, wenn der Button tatsächlich null ist? Dann crasht das Programm nicht. Aber es funktioniert auch nicht. Vielmehr passiert stillschweigend nichts. Und das ist ganz bestimmt sehr, sehr überraschend für den Benutzer. Daraus kann nur ein hässlicher Supportfall werden. Wer führt über diese Kosten Buch? Der Entwickler ist fein raus: kein Crash. Doch der Rest der Organisation hat Schweiß auf der Stirn. Die Uhr tickt.

Das ist großer Mist.

Mehr Vertrauen!

Defensive Programmierung ist vielleicht ein Mittel in einer Welt von Einzelkämpfern, die niemandem vertrauen können. Es ist eine letzte Zuflucht in einer feindlichen Umgebung.

Doch das kann doch nicht die Vorstellung von einem produktiven Arbeitsplatz sein. Die Zusammenfassung von Menschen in einem Team, ihre Anstellung in einem Unternehmen soll ja gerade Kosten durch Misstrauen vermeiden. Unternehmen, Teams, Projekte definieren Horizonte des Vertrauens. Das dient der Reduktion von Komplexität.

Menschen, die sie in einer Kneipe treffen, untersuchen Sie auch nicht erst auf Waffen. Wechselgeld im Supermarkt prüfen Sie auch nicht erst auf Falschgeld. Zwischen vielen Ländern in Europa sind sogar die Grenzkontrollen abgeschafft. Sie können nach Dänemark oder in die Schweiz fahren, ohne eine Prüfung Ihrer Identität.

Solches Vertrauen macht das Leben einfacher. Wenn Sie einen Eindruck davon bekommen wollen, wie es zugeht, sobald wir dieses Vertrauen nicht mehr haben, lesen Sie z.B. dies. Eine schreckliche Welt!

Also: Die Bequemlichkeit und Effizienz der Welt, die wir schätzen, hängt von der Reichweite unseres Vertrauens ab.

Das ist uns im täglichen Leben bewusst, deshalb sind wir bereit, dafür etwas zu tun. Als Gesellschaft geben wir einiges Geld aus für Organe, die solche Verhältnisse erhalten. Polizei, Gerichte, Gesundheitsamt, TÜV sind nur einige.

Nur die Softwareentwicklung scheint das noch nicht begriffen zu haben. Denn nach Aussage des Teilnehmers, der für mich der Anlass zum Poltern war (Sorry, again!), hat er die defensive Programmierung in der Ausbildung beigebracht bekommen. Das Misstrauen ist also Lehrmeinung. Der Wilde Westen lebt noch in den Köpfen der Lehrer. Kein Entwickler ist sicher vor einem anderen, deshalb müssen alle fleißig proaktive Verteidigungsmaßnahmen ergreifen?

Wie nun aber richtig?

  1. Der Kunde bestimmt, was Anforderung ist.
  2. Vertrauensgrenzen sind explizit zu ziehen.

Vertrauensgrenze Benutzerschnittstelle

Der vornehmste Grund, um ein Validation von Input vorzunehmen ist der Wunsch des Kunden. Das bedeutet, als Entwickler müssen wir mit dem Kunden reden. Horribile dictu! Ja, reden. Wir müssen mit ihm über Schnittstellen reden. Nur an denen ist er ja interessiert. An Schnittstellen zeigt sich für ihn das Verhalten der Software, die er beauftragt.

Die offensichtlichste Schnittstelle ist die Benutzerschnittstelle: Menschen geben Daten ein, die Software antwortet mit Daten. Aber auch Service-Schnittstellen gehören dazu. Die werden von anderer Software genutzt, drehen sich jedoch auch nur um Input-Daten (request) die in Output-Daten (response) transformiert werden sollen.

Mit dem Kunden ist abzusprechen, welcher Input zu welchem Output führen soll. Der Kunde ist in diesem Fall wirklich mal König. Der zahlt, der sagt an. Das war im obigen Beispiel eindeutig der Fall.

Das bedeutet: Was er nicht ansagt, dafür will er im Zweifelsfall nicht zahlen oder noch nicht zahlen. Wenn Entwickler trotzdem Code schreiben für Verhalten, das der Kunde nicht gewünscht hat, dann verschwenden sie sein Geld. Massiv. Trotz allerbester Absichten. Sie handeln im besten Fall paternalistisch, im schlechteren arrogant.

Die korrekte Verhaltensweise für Entwickler ist: Wenn Sie eine mögliches Validitätsproblem sehen, dann melden sie das dem Kunden. Es gilt das ehrwürdige militärische Prinzip: Melden macht frei.

Ich kritisiere also nicht den Gedanken „Hier könnte aber etwas falsch laufen, wenn null übergeben würde.“ Im Gegenteil: Das ist sensibel beobachtet! Sehr gut!

Nur aus dem guten Gedanken ist dann im Training keine gute Maßnahme geflossen. Die wäre gewesen, mich als „Kunde“ darauf anzusprechen. Dann hätte ich wiederholen können, dass ich (derzeit) keine Behandlung invalider Daten wünsche oder hätte mich überzeugen lassen, dass das eine gute Idee ist. In dem Fall hätten wir zumindest ein Beispiel für unter diesen Umständen von mir erwünschtes Verhalten notiert und ggf. den syntaktischen Kontrakt angepasst.

Der typische Einwand gegen dieses Vorgehen: „Ich kann den Kunden doch nicht mit all solchen Kleinigkeiten belasten. Der erwartet, dass ich sowas selbstständig mache.“

Da ist etwas dran. Der Kunde hat tatsächlich eine Erwartung an unsere Kompetenz als Entwickler. Doch die ist nicht, dass wir alle Sicherheitsmaßnahme selbstständig ergreifen. Das gäbe es nämlich kein Halten mehr. Sicherheit ist unersättlich. Ohne Kontrolle stürzen wir in ein Sicherheitskaninchenloch.

Die Erwartung des Kunden ist vielmehr – zumindest, wenn er kostenbewusst ist -, dass wir ihn informieren. Mit unserer Kompetenz sollen wir ihn auf mögliche Probleme aufmerksam machen. Dann kann er entscheiden, wie zu verfahren ist. Nur so sichern wir uns auch wirklich ab gegen spätere Kritik des Kunden.

Kunden haben Prioritäten. Da mag eine Input-Behandlung mit Lücken dringender sein als eine ohne. Denn mit Lücken ist ein Durchstich schneller hergestellt. Entscheidet ein Entwickler selbstständig, lückenlos zu programmieren, sinkt die Geschwindigkeit in Bezug auf das, was dem Kunden am Herzen liegt.

Der Aufwand für defensive Programmierung ist ja nicht zu unterschätzen. Das ist Logik. Die muss zuerst hingeschrieben werden, dann muss sie getestet werden. Das sind pro Funktion 2-3 Minuten. Bei 10 Funktionen sind es schon 30 Minuten. Wenn das 3 Entwickler pro Tag tun, sind es 90 Minuten. Schnell werden also Hunderte Euro durch defensive Programmierung verbrannt, die kein Kunde gewünscht hat.

Kleine Übung: Wer defensiv programmiert, tut einfach 5€ pro Maßnahme in die Teamkasse. Wie wäre das? :-)

Dazu kommt der Aufwand beim Lesen. Vielleicht ist der defensive Code noch schnell hingeschrieben. Doch er wird ja viel, viel häufiger gelesen als geschrieben. Jedes Mal ist er dann zu verstehen. Das kostet einen kleinen Aufwand. Defensiver Code verrauscht den eigentlichen Code. Er ist dessen Verständlichkeit abträglich. Die Wandelbarkeit von Software sinkt also durch defensiven Code, der ohne Auftrag geschrieben wird. Mittelfristig schießt sich der Entwickler, der ihn schreibt, damit sogar in den eigenen Fuß.

Vertrauensgrenze Modulschnittstelle

Absicherungscode, der einer expliziten Kundenanforderungen folgt, ist nun eigentlich gar kein defensiver Code mehr. Er ist ausdrücklich gewünscht. Es findet keine ängstliche Selbstverteidigung mehr statt.

Echt defensiver Code ist demgegenüber nie vom Kunden ausdrücklich gewünscht. Er erfüllt keine funktionale Anforderung und auch keine nicht-funktionale Effizienzanforderung. Vielmehr ist er zur Herstellung der Investitionssicherheit gedacht und fällt damit in einen Bereich, den der Kunde eher nicht im Blick hat. Defensive Programmierung ist eine verzweifelte Maßnahme Einzelner, um noch etwas Korrektheit und Produktivität zu retten. Ach, wie tragisch…

Doch woher kommt diese Verzweiflung? Woher das Misstrauen? Es ist ein Resultat mangelnder Kommunikation. Denn da, wo nicht kommuniziert wird, da bleiben und entstehen weiße Flecken. Man weiß nicht, was der andere denkt und tut. Unsicherheit greift Raum, dann Misstrauen – allemal wenn es zu negativen Erfahrungen kommt -, schließlich unternimmt man persönliche Absicherung.

Das, was sich auf gesellschaftlicher Ebene abspielt, spiegelt sich im Kleinen in Teams, in denen nicht ausreichend kommuniziert wird.

In Dresden und anderswo mag es helfen, zu versuchen, Misstrauen durch mehr Gespräche in Kaffeeküchen zu überwinde. Integration beginnt mit einem freundlichen Wort und einem offenen Ohr.

Für Softwareteams reicht das jedoch nicht aus. Sie bedürfen systematischerer Kommunikation.

Test-first

Die erste Maßnahme in dieser Hinsicht ist test-first Codierung. Tests sind die zentrale Dokumentation für gewünschtes Verhalten. Wer nachschauen will, wie Software benutzt werden soll, der studiert automatisierte Testfälle. Sie stellen eine lebendige Dokumentation des Soll-Zustands dar. Damit dienen sie nicht nur der Absicherung gegen Regressionen, sondern auch der Kommunikation im Team. Niemand soll sagen können, er hätte nicht gewusst, wie korrekte Parameter für eine Funktion aussehen.

Und wenn die vorhandenen Tests eine Frage zum Softwareverhalten nicht beantworten… dann schreibt man einen weiteren Test, der exploriert, wie sich Software de facto bisher verhält. Vielleicht ist das schon ausreichend, vielleicht regt das aber auch weitere Kommunikation an.

Nicht nur automatisierte Tests sind jedoch wichtig, sondern sogar sie zuerst zu schreiben, also vor dem Produktionscode. Erstens werden sie dann nicht so leicht vergessen, zweitens bietet ihre Codierung Gelegenheit, sich aus Sicht eines Softwarenutzers Gedanken darüber zu machen, was denn eigentlich passieren soll. Man lügt sich weniger in die Tasche durch schon vorhandene Kenntnis „der Innereien“. Weniger Bestätigungsfehler.

Entwurf

Und woher kommen die Schnittstellen, auf die automatisierte Tests angesetzt werden? Sie sind das Ergebnis eines expliziten Entwurfs. Womit ich beim großen Sorgenkind der Softwareentwicklung bin.

Im Fehlen eines expliziten Entwurfs, der diesen Namen verdient, sehe ich den Hauptgrund für die Praxis der defensiven Programmierung. Es wird nicht gelehrt, wie explizit und systematisch vor der Codierung entworfen werden kann. Damit entsteht in der Kommunikation im Team eine eklatante Lücke. Einzelne Entwickler werden zu schnell sich selbst überlassen. Das treibt sie in die Unsicherheit. Dann wollen sie ihren Arsch absichern.

Ein systematischer Entwurf von Software durch das Team bestimmt die wesentlichen Module zur Realisierung anstehender nicht-funktionaler Anforderungen. Sein Ergebnis sind Funktionen, Klassen, Komponenten, Services mit klaren Schnittstellen und Beziehungen.

Weniger geht gar nicht.

Und dann werden in dieser Struktur ausdrückliche Vertrauensgrenzen gezogen. Das Team definiert gemeinsam, zwischen welchen Modulen Misstrauen herrschen soll. Denn Misstrauen hat auch etwas Gutes: es entkoppelt.

Dadurch entsteht eine interne Anforderung zur Herstellung von Wandelbarkeit. Die ist zu erfüllen mit Code, der über die Verhaltensanforderungen des Kunden hinausgeht. Das ist völlig ok, ja sogar notwendig. Das erwartet der Kunde zurecht vom Team.

Doch Achtung: Es ist eine Teamentscheidung, wo diese Grenzen verlaufen. Einzelne Entwickler kompensieren hier wieder nicht ihre persönliche Unsicherheit. Deshalb würde ich auch in diesem Fall nicht von defensiver Programmierung sprechen.

Selbstverständlich hält das Team solche Entscheidungen fest: in Form von syntaktischen und semantischen Kontrakten. Nichts anderes sind Funktionssignaturen bzw. Interfaces und Tests.

Interne Vertrauensgrenzen manifestieren sich damit geplant im Code. Sie sind keine ad hoc Entscheidungen. Einzelne Entwickler sind bei der Codierung von Logik in Funktionen nur für deren Korrektheit zuständig. Ihre Aufgabe ist es in dem Moment nicht, darüber zu spekulieren, wie irgendwer diese Funktion benutzen könnte.

Nutzungsspekulationen sind Sache des Entwurfs und damit des Teams.

Fazit

Schluss mit defensiver Programmierung! Sie ist ein Relikt aus Zeiten, da man noch glaubte, man müsse als Softwareentwickler nicht reden. Doch kein Softwareentwickler ist eine Insel.

Softwareentwicklung ist ein „Teamsport“. Der funktioniert nur, wenn man sich vertraut. Dazu gehört gekonnte Kommunikation und geeignete Dokumentation von Entscheidungen.

Wo defensive Programmierung noch an der Tagesordnung ist, da fehlt es an diesen Kompetenzen. Das Resultat: geringe Produktivität, Verschwendung von Kundengeldern. Traurig, traurig.

Doch zum Glück gilt: Gefahr erkannt, Gefahr gebannt. Es ist nie zu spät, mit besserer Kommunikation zu beginnen und untaugliche Gewohnheiten abzulegen. Now is the time!

Entwurf und test-first Entwicklung lassen sich lernen. Es winkt Entspannung durch mehr Vertrauen.

Software in Gerüsten bauen

Softwareentwickler machen keinen Dreck. Softwareentwickler tun auch nichts Überflüssiges. So scheint mir jedenfalls der Anspruch.

Die Realität sieht am Ende natürlich ganz anders aus: der Dreck ist im Code überall, es stinkt mächtig (code smells). Und vom Überflüssigen ganz zu schweigen. Wenn premature optimization laut Donald Knuth „the root of all evil“ ist, dann findet sich Überflüssiges allerorten im Code.

Natürlicher Müll

Woher kommt dieser Anspruch aber? Andere Berufe haben kein Problem, Dreck zu machen und Überflüssiges in Form von (temporären) Hilfskonstruktionen herzustellen. Das tun sie ganz bewusst. Es ist geradezu sprichwörtlich: „Wo gehobelt wird, da fallen Späne.“ Genau! Das ist auch nicht schlimm, wenn man am Ende der Arbeit die Späne zusammenfegt und entsorgt.

Überall entsteht Abfall in irgendeiner Form während Dinge hergestellt wird. Der ist eigentlich überflüssig, lässt sich jedoch trotz fleißigen Bemühens nicht komplett vermeiden. Späne, Bauschutt, Nahrungsmittelabfälle, Abwasser… Die Transformationseffizienz ist eben nicht 100%. Input kann nicht perfekt in Output überführt werden.

Doch wo ist solch bewusst in Kauf genommener Dreck, Abfall, Müll bei der Softwareentwicklung? Ich sehe ihn nicht. Es entsteht zwar Dreck, nur unbewusst. Er wird nicht als solcher wahrgenommen. Das halte ich für ein Problem. Nur TDD hat dafür ein Auge: im dritten Schritt nach Red und Green soll mit Refactoring aufgeräumt werden. Leider wird selbst das aber nur selten getan.

Softwareentwickler durchlaufen keine Lehre, in der ein fester Bestandteil das Aufräumen ist. Tischlerlehrlinge, Kochlehrlinge, Schlosserlehrlinge hingegen lernen alle die (Wieder)Herstellung von Ordnung am Ende von Arbeitsphasen. Dreck wird zusammengekehrt, Werkzeug wird in Schubladen und Regale weggeräumt. In einer Küche ist das sogar extrem. Da wird nicht bis zum Tagesende gewartet, sondern ständig gewischt und gespült. Während die eine Hand mit dem Kochlöffel rührt, putzt die andere schon entstandene Spritzer weg. Das ist ein simples Gebot der Hygiene, wenn schon nicht der Produktivität.

Ich finde das ganz natürlich. Sogar würde ich sagen, Abfall bewusst und kontrolliert entstehen zu lassen, ist eine Sache der Effizienz. Denn wenn ich in jeder Sekunde sauber sein muss, wenn ich nie Dreck erzeugen darf… dann werde ich langsam, weil ich ja ständig nicht nur meine eigentliche Aufgabe verfolgen muss, sondern auch auf permanente Sauberkeit zu achten habe.

Sauberkeit und Ordnung sind wichtig – aber nicht in jeder Sekunde. Phasen, in denen Späne fallen, und Phasen, in denen die Späne zusammengefegt werden, dürfen, ja sollten sich sogar abwechseln. Solche Natürlichkeit ist allerdings aus meiner Sicht weder im Bewusstsein der meisten Entwickler noch in dem ihrer Vorgesetzten gewachsen.

Hilfreiche Konstruktionen

Dasselbe gilt für Hilfskonstruktionen. Die werden auch als überflüssig angesehen. Warum nur? Weil sie von vornherein als Abfall erscheinen und daher vermieden werden müssen? Dabei arbeitet der Rest der Welt damit ständig: Häuser werden eingerüstet, Schiffe auch, Autos kommen auf Hebebühnen, Schiffe auch, es gibt Kräne, es gibt Schuttrohe am Bau usw. usf.

Diese Hilfskonstruktionen sind dazu da, die Arbeit am Werkstück zu erleichtern. Und am Ende werden sie abgebaut. Das Haus steht wieder frei, das Auto auf der Straße, das Schiff liegt im Wasser. Nichts mehr zu sehen von den so nützlichen Hilfskonstruktionen. Man war sich nicht zu fein, zusätzlichen Aufwand für deren Auf- und Abbau zu treiben. Denn dadurch konnte die eigentliche Arbeit viel effizienter gestaltet werden. Unterm Strich also ein Gewinn.

Nur die Softwareentwicklung kennt das nicht. Dort gibt es keine Hilfskonstruktionen, die bewusst temporär aufgebaut und nach der eigentlichen Arbeit wieder abgebaut werden. Selbst bei TDD, wo ja sogar Abfallbeseitigung „eingebaut“ ist, sind Hilfskonstruktionen unbekannt. Alle Tests sollen erhalten bleiben. Aber warum? Gibt es denn keine Unterschiede bei Tests? Ich sehe sehr wohl Tests, die eigentlich nur temporär nützlich sind. Die nenne ich Gerüsttests – denn ich baue sie nach getaner Arbeit wieder ab. Der Gewinn: der Code wird übersichtlicher, die Angriffsfläche für Regressionen wird verringert.

Dabei gibt es das Konzept des Prototyps in der Softwareentwicklung. Nur wird der regelmäßig entgegen der Begriffsdefinition nicht weggeschmissen, sondern zum Produkt umgebaut.

XP kennt spike solutions. Auch das Code, der am Ende weggeworfen wird. Nur wer „spiket“ denn bewusst? Das scheint mir selten trotz immer wieder hoher Unsicherheiten im Verständnis von Anforderungen oder beim Einsatz von Technologien.

Seit Jahren gibt es auch das Konzept Kata, also Übungsaufgabe. Entwickler treffen sich gelegentlich, um gemeinsam an Katas etwas zu lernen. Doch das sind Einzelne, vielleicht höchstens 5% aller Entwickler. Denn eines ist ja klar: der Code zu einer Übungsaufgabe ist am Ende Abfall. Der kann weg, ist nicht im Tagesgeschäft einsetzbar. Also denken sich 95% der Entwickler, dass es wohl nicht lohne, ihn überhaupt zu schreiben. Der ist überflüssig.

Ab auf die Werkbank!

Vielleicht ist ja ein Grund für die „Abfallaversion“, dass die Softwareentwicklung auch keinen Unterschied machen zwischen dem Ort, wo ein Teil bearbeitet wird, und dem, wo das Teil in einem Ganzen eingebaut ist. Softwareentwicklung findet immer am endgültigen Marmorblock statt. Alles wird aus einem Stück herausgemeißelt. Kein Wunder, dass der Softwaremonolith allgegenwärtig ist.

Das mag ja gut gehen, wenn man Michelangelo heißt und wahrhaft meisterlich mit Marmor umgehen kann. Doch normale Entwickler sollten sich doch eigentlich nicht an so einer Aufgabe probieren. Viel einfacher ist es, ein Ganzes aus Teilen zusammenzusetzen, die man einzeln auf Werkbänken bearbeitet. Doch das geschieht nicht. Code, der zum Einsatz beim Kunden kommt, ist immer eingebaut im Ganzen. Man kann vielleicht auf ihn von außen mit Tests zugreifen, es gibt also Revisionsklappen. Doch das ist ein Zugeständnis an das Paradigma „aus einem Stück hauen“.

Bei TDD as if you meant it wird dieses Paradigma aufgebrochen – doch wer kennt diesen TDD-Ansatz?

***

Abfall, Hilfskonstruktionen, Werkbänke… das alles sind keine Themen in der Softwareentwicklung. Mir scheint das ein Problem zu sein. Der schlechte Stand der inneren Codequalität ist für mich umso weniger ein Wunder. Wenn wir uns die Freiheit nicht nehmen, etwas „Verschwendung“ zuzulassen, um dafür auf der anderen Seite fokussierter und effizienter zu arbeiten, dann machen wir noch keinen guten Job als Software Craftsmen oder Software Engineers.

Frühjahrsputz 2016 – Gemeinsam ran an den Schmutz

Der Frühjahrsputz für Wohnung und Körper (Fasten) ist Tradition. Warum nicht diese schöne Tradition auch auf Ihre Codebasis ausdehnen? Dort gibt es sicher auch einige Ecken, die mal entrümpelt werden könnten, oder?

Wenn Sie jetzt denken „Ja, müsste ich mal machen…“, sich aber nicht aufraffen können, dann verstehen wir das allerdings sehr gut. Aufräumen macht irgendwie keine Freude. Oder wenn doch, dann fällt es Ihnen trotzdem wahrscheinlich schwer, dafür einen Termin im Tagesgeschäft zu finden.

Deshalb bieten wir Ihnen an: Wir helfen Ihnen beim Frühjahrsputz!

Gehen Sie Staub, Schmutz, Gerümpel in Ihrem Code nicht allein an. Lassen Sie uns das zusammen machen: „Pair Putzen“ :-) Gemeinsam macht es mehr Freude; zu zweit sieht man auch mehr Schmutzstellen und ein Termin mit jemand anderem wird leichter eingehalten.

Die CCD School Putzhilfeaktion

Ja, Sie haben richtig gelesen: Wir würden Ihnen gern beim Säubern Ihres Codes zur Hand gehen. Sauberer Code, also Clean Code, ist immerhin unser Job ;-)

Und weil die Sonne so schön lacht, die Vöglein so munter zwitschern und die Kirsche schon so hübsch blüht, haben wir uns ein Angebot überlegt, dem kein Schmutz widerstehen kann. Wie bieten…

  • 6 Teams
  • jeweils 3 Termine
  • à 90 Minuten

unsere Hilfe an – und das lediglich zum Preis eines Amazon-Gutscheins, dessen höhe sie selbst am Ende der Putzaktion bestimmen.

So funktioniert’s:

Bewerben Sie sich mit Ihrem Team über das unten stehende Formular, wenn Ihre Codebasis in Java oder C# entwickelt ist. Aus allen Bewerbungen wählen wir 6 aus, denen wir beim Frühjahrsputz jeweils insgesamt 4,5 Stunden beistehen. Diese Zeit verteilen wir allerdings auf 3 online Sitzungen à 90 Minuten. In den Sitzungen arbeiten wir im remote pair via Teamviewer und anderen Werkzeugen zusammen an der realen Codebasis.

Die Sitzungen liegen 1-2 Wochen auseinander, so dass die ausgewählten Teams zwischendurch Zeit haben, mit den Impulsen aus dem „Pair Putzen“ selbstständig weitere Erfahrungen zu sammeln.

Um Ihnen die Bewerbung leicht zu machen, verzichten wir in diesem schönen Frühling auf ein Honorar. Wenn ein Team unsere Unterstützung jedoch als wertvoll empfunden hat, dann freuen wir uns über eine Anerkennung in Form eines Amazon-Gutscheins. Dessen Höhe bestimmt das Team selbst nach Budget und Wertempfinden.

Unsere Motivation

In Zeiten so vieler scheinbar kostenloser Angebote mögen Sie sich fragen, warum wir Ihnen dieses Angebot machen. Der Grund ist einfach: Wir wollen mehr Erfahrung beim Säubern von realem schmutzigem Code machen. In unseren Trainings konzentrieren wir uns bisher auf Clean Code, wollen also Schmutz von vornherein vermeiden. Und Beratungseinsätze bringen uns zwar ans Brownfield, an den Legacy Code, in den Big Ball of Mud – doch da solche Begleitungen länger dauern, gibt es davon nicht so viele verschiedene.

Um in der Vermittlung von Clean Code Prinzipien und Praktiken noch besser zu werden, glauben wir, müssen wir jedoch mehr, viel mehr unsauberen Code sehen. Diese Putzaktion ist ein Versuch, dieses Ziel zu erreichen.

Wir helfen Ihnen beim Reinemachen, Sie helfen uns mit Zugang zu realem Legacy Code. So einfach ist der Deal.

Los geht’s

Wenn Sie Lust auf unsere Unterstützung beim Code-Frühjahrsputz haben, dann füllen Sie das folgende Formular aus. Bewerbungen sind bis zum 20.4.2016 möglich. Selbstverständlich behandeln wir alle Bewerbungen vertraulich und begraben Sie anschließend nicht unter Spam-Emails ;-)