Follow my new blog

Posts mit dem Label Clean Code Developer werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Clean Code Developer werden angezeigt. Alle Posts anzeigen

Dienstag, 27. Januar 2015

Die Selbstlern-Challenge - Mitmachen und gewinnen!

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

Die Herausforderung

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

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

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

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

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

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

Update: Seit 28.1.2015 sind alle Teilnehmerplätze vergeben!

Die Teilnahme

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

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

Der Gewinn

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

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

Die Hypothese

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

 

Und nun: Lasst das Selbstlernen beginnen!

Donnerstag, 2. Oktober 2014

Responsibilities zählen

Das Single Responsibility Principle (SRP) ist eine Säule sauberer Softwareentwicklung für hohe Wandelbarkeit. Nicht umsonst macht es auch den Anfang bei den SOLID Prinzipien, würde ich sagen.

Aber: Wie finden Sie denn heraus, wieviele Responsibilities (Verantwortlichkeiten) eine Methode oder Klasse hat? "Naja, das sieht man halt", scheint mir ein zu schwammiges Kriterium für ein so zentrales Prinzip.

Was ist eine Verantwortlichkeit?

Bevor es mit dem Zählen losgehen kann, muss natürlich klar sein, was da überhaupt entdeckt und gezählt wird. Was ist eine Verantwortlichkeit?

Die Literatur spricht von "only one reason to change". Das finde ich leider nicht wirklich hilfreich. Denn um welche Gründe geht es, worauf beziehen die sich?

Ich versuche es daher einmal so:

Jede Methode soll nur Anweisungen enthalten, die Anforderungen eines Aspekts erfüllen.

Diese Definition ist knackiger, erfordert jedoch die Klärung zweier Begriffe:

  • Aspekt: Ein Aspekt ist eine Menge von zusammengehörigen Merkmalen, die sich unabhängig von anderen Merkmalen ändern können. Man könnte einen Aspekt auch eine Dimension nennen. Beispiele für Aspekte in der realen Welt sind z.B. Frisur, Kleidung, Bildung. Sie können die Merkmale Ihrer Frisur (Haarfarbe, Haarlänge, Schnitt) unabhängig von Merkmalen Ihrer Kleidung (Stoff, Jahreszeitlichkeit, Stil) oder Bildung (Dauer, Inhalt, Ort) ändern.
  • Anweisung: Anweisungen sind Sprachkonstrukte, die definieren, was eine Software tun soll. Sie fallen für mich in zwei Kategorien: Logik und Integration.
    • Logik: Mit Logik bezeichne ich die essenziellen Anweisung von Programmiersprachen, die der Erfüllung von funktionalen wie qualitativen Anforderungen dienen. Das sind Operatoren, die Daten transformieren, Kontrollstrukturen (z.B. if, while), die den Verarbeitungsfortgang steuern, und Hardwarezugriffe (vermittels API-Aufrufen). Logik beschreibt Algorithmen.
    • Integration: Integration bindet mehrere Methoden zu einem Datenfluss zusammen.

Ich denke, jetzt wird auch verständlicher, was "only one reason to change" bedeutet: Methoden sollen nur geändert werden müssen, wenn sich an einem Anforderungsaspekt etwas verändert hat.

Methoden sind die kleinsten Container von Programmiersprachen. Darüber liegen für mich in wachsender Größe Klassen, Bibliothekn, Komponenten und µServices. Für die muss das SRP natürlich auch gelten. Also lautet es ganz allgemein:

Jeder Container soll nur Anweisungen enthalten, die Anforderungen eines Aspekts erfüllen.

So zumindest das Ideal. In der Praxis kann und muss sogar davon temporär oder in überschaubarem Maße abgewichten werden. Ziel sollte jedoch ein einziger Aspekt pro Container sein.

Außerdem ist zu bedenken, dass Aspekte in Hierarchien existieren. Zum Aspekt der Kleidung mögen die Merkmale Stoff, Jahreszeitlichkeit und Stil gehören. Nur zum Stoff z.B. gehören dann jedoch weitere Sub-Aspekte wie Farbe, Material, Haptik. Kleidung kann aus grobem roten Leinen oder feiner grüner Seide oder rauer gelber Wolle oder feinem gelbem Leinen usw. bestehen.

Ein Container auf höherer Abstraktionsebene (z.B. Klasse) kann dann für einen Dachaspekt stehen, der Container von Sub-Aspekten (z.B. Funktionen) zusammenfasst.

Härtegrade

Für mich gibt es Aspekte in unterschiedlichen Härtegraden. Hart sind Aspekte, die man an der Form erkennt. Man muss die Anforderungen nicht verstehen, die Anweisungen erfüllen, sondern nur die Programmiersprache/Plattform.

Beispiele hierfür sind die Anweisungsaspekte Logik und Integration sowie der Aspekt Datenstruktur.

Innerhalb der Logik können dann jedoch weitere verschiedene Hardwarezugriffe unterschieden werden, z.B. Tastatureingabe, Bildschirmausgabe, Dateisystemzugriff, Datenbankzugriff. Und sogar den Zugriff auf den Heap würde ich dazurechnen, also den Umgang mit Hauptspeicher. Dazu ist zwar kein spezieller API nötig, doch Zugriffe aus globale Daten (statische Felder oder Felder von Objektinstanzen) sind klar erkennbar.

Weich hingegen sind Aspekte, bei denen man verstehen muss, worum es geht. Es geht um das Was, wohingegen harte Aspekte das Wie betreffen.

Schon die Unterscheidung zwischen lesender und schreibender Logik setzt Interpretation voraus. Es ist daher kein Wunder, dass bei weichen Aspekten schnell Diskussionen entstehen. Der eine empfindet Lesen und Schreiben als Merkmale des selben Aspekts, der andere empfindet sie als getrennt - was sich dann jeweils in unterschiedlicher Aufteilung in Container ausdrückt.

Auch hier wieder eine Hierarchie: Lesen und Schreiben sind z.B. Sub-Aspekte von Objektpersistenz (zu der auch z.B. Serialisierung gehört). Und Objektpersistenz ist ein weicher Aspekt von z.B. Personalisierung. Die wiederum ein Aspekt des Anforderungsaspektes Usability ist - welche zur Anforderungskategorie Qualität gehört.

Und was folgt aus der Unterscheidung zwischen harten und weichen Aspekten? Halten Sie sich so lange wie möglich bei der Strukturierung von Code an harte Aspekte. Darüber gibt es viel weniger Diskussion. Das macht Ihre Codierung schneller, das macht Reviews konfliktfreier.

Am Ende jedoch können Sie natürlich den weichen Aspekten nicht ausweichen. Üben Sie also immer wieder Ihre Sensibilität in der Unterscheidung und Zusammenfassung von weichen, inhaltlichen Merkmalen.

Apropos Üben...

Angewandte Aspekterkennung

Zum Abschluss ein Codebeispiel, an dem ich Ihnen die Identifikation von Aspekten praktisch demonstrieren möchte. Ich entnehme es dem Buch "Head First C#".

image

Das Szenario? Versuchen Sie es doch einmal dem Code zu entnehmen. Wie Sie feststellen werden, ist das jedoch schwierig. Weil er nicht integriert und kein Titel sichtbar ist. Die Methode Main() enthält ausschließlich Logik. Und die hat ihrer Natur nach denkbar wenig Dokumentationsqualität. Reine Logik muss immer entziffert werden. Hobbyarchäologen sind klar im Vorteil ;-)

Aber ich verrate Ihnen das Szenario: Es handelt sich um eine Anwendung zur Darstellung von Dateiinhalten in hexadezimaler Form.

Hier nun der von mir mit Aspekten kommentierte Quellcode:

image

Das Kapitel im Buch heißt "Dateien lesen und schreiben" - aber wie das geht, muss der Leser sich mühsam im ganzen Code zusammensuchen. Nicht nur wie das Problem ganz allgemein gelöst wird, wie der Prozess aussieht, der das gewünschte Verhalten herstellt, ist also unklar. Es wird auch dem technologisch Interessierten schwer gemacht, sich zu informieren.

Das ist Bullshit. Das ist dirty code par excellance. Niemandem ist mit soetwas gedient. Und das in deinem Lehrbuch...! Erschreckend.

Ich habe vier Aspekte identifiziert, die über den Code verstreut und auch noch geschachtelt sind. Das ist das Gegenteil von Entkopplung.

Die Domäne ist die Formatierung von Bytes in hexadezimale und ASCII Darstellung. Aber weder ist diese Domäne als ein für sich stehendes Stück Logik herausgearbeitet, noch die anderen Aspekte.

Aber ich will Goethes "Besser machen, nicht nur tadeln, soll den rechten Meister adeln" folgen und Ihnen nicht vorenthalten, wie ich meine, das der Code aussehen sollte.

In der reinen Logik unterscheidet sich meine Lösung nur unwesentlich von der im Buch. Doch ich habe die Logik anders (bzw. überhaupt) in Container verpackt. Zunächst nur in Funktionen:

image

Das ist ein erster Schritt. In Main() ist der Prozess nun deutlich sichtbar. Außerdem ist klar, wo welche APIs benutzt werden. Wer sich zum Thema "Dateien lesen und schreiben" informieren will, schaut einfach bei Check_if_file_exists() und Read_blocks_from_file() rein; den Rest kann man dann ignorieren.

Wem das nun jedoch zu wenig objektorientiert ist, wer gern noch eine deutlichere Zusammenfassung der Subaspekte sehen möchte, der findet hier eine Lösung mit Klassen:

image

Zum Beispiel bündelt die Klasse FileSystemProvider die Methoden, in denen sich Logik befindet, die den API System.IO benutzt.

Die Aspekttrennug ist damit deutlicher. Der Preis dafür ist etwas Rauschen: in Main() müssen nun Klassen instanziert werden und Methodenaufrufe haben Objektnamen als Präfix.

Überhaupt haben meine Lösungen doppelt oder gar mehr als doppelt so viele LOC (lines of code). Ist das gut? Sollte Code nicht immer so kurz und knapp wie möglich sein? Trägt Knappheit nicht zur Lesbarkeit bei?

Klar, wenige Zeilen Code lassen sich leichter überschauen, sozusagen physisch. Aber inhaltlich ist das nicht unbedingt der Fall, nämlich wenn es sich um reine Logik handelt. Das war ja das Problem des ursprünglichen Codes. 40-50 LOC, also rund eine Bildschirmseite, das war nicht viel - und doch war es nur schwer verständlich.

Da bezahle ich gern den Preis von etwas Rauschen und mehr LOC, wenn ich dafür Verständlichkeit bekomme. Und die ist nun vorhanden, würde ich sagen.

Der Einstieg ins Programm ist sonnenklar. Er beschreibt lesbar, wie das Verhalten "Dateiinhalt als Hex Dump anzeigen" hergestellt wird:

image

Main() hat nun eine einzige, harte Verantwortlichkeit: Integration.

Und jede Klasse hat wiederum nur eine einzige Verantwortlichkeit, z.B. FilesystemProvider:

image

Sie kapselt die Nutzung des System.IO API. Dieser Aspekt zerfällt jedoch in zwei weiche: Prüfen, ob eine Datei existiert, und blockweises Lesen der Bytes aus einer Datei.1

Fazit

Das Single Responsibility Principle ist zentral für saubere Softwareentwicklung. Um es anwenden zu können, muss man allerdings wissen, was denn eine Responsibility eigentlich ist.

Mit der Definition, die ich gegeben habe, fällt es Ihnen hoffentlich leichter, die Verteilung von Responsibilities in Ihrem Code zu überschauen - und sie dann mit Refaktorisierung zu entzerren.


  1. Ok, ich gebe zu, ein kleinwenig unsauber ist der Code noch. Denn sowohl in Check_if_file_exists() wie in Get_filename() nutze ich zwei APIs. Zum einen den eigentlichen, um den es dort geht (Dateisystem- bzw. Kommandozeilenzugriff), zum anderen jedoch auch den für die Ausgabe auf der Konsole zur Fehlermeldung. Konsequenterweise müsste ich die Fehlermeldung in eine eigene Methode verpacken und z.B. in den ConsoleProvider verschieben. Eigentlich - denn hier lasse ich das mal so stehen. Es ist eine vergleichsweise kleine Sünde. Und wer will schon Perfektion? :-) Wenigstens bin ich mir der “Schmutzrückstände” bewusst.

Mittwoch, 1. Januar 2014

Unordnung muss sein

imageNeulich habe ich mit meiner Freundin geheimwerkert. Sie wollte ihr Wohnzimmer mit einigen Fotos in selbstgebastelten Bilderrahmen anreichern. Holzprofile zuschneiden und zusammensetzen zu Rahmen, war eine Sache. Eine andere, die Bilderleiste im Wohnzimmer anzubringen, an der die Rahmen aufgehängt werden können. (Eine Bilderleiste statt Nägeln in der Wand für jeden Rahmen sollte es sein, damit die Rahmen flexibler gehängt werden können.)

Bei der Montage der Bilderleiste habe ich dann eine Erkenntnis gehabt:

Unordnung ist bei Veränderungen nicht zu vermeiden.

Wer Rahmen bastelt, eine Bilderleiste anbringt, einen Weihnachtsbaum herrichtet, ja, sogar wer aufräumt, erzeugt Unordnung. Das ist nicht zu vermeiden. Auch wenn am Ende die Entropie niedriger sein soll, wird sie zunächst erhöht.

Hier das Wohnzimmer während der Arbeit an den Bilderleisten in seiner ganzen Unordnungspracht:

image

Die Sitzbank ist bis aufs Holz abgeräumt, der Wohnzimmertisch ein Werkzeuglager, der Sessel beiseitegeschoben und mit Krimskrams beladen, Staubsauger und Bohrmaschine lungern in der Gegend herum… Nein, das ist nicht die übliche niedrig-entropische Gemütlichkeit.

Am Ende jedoch, wenn die Veränderungen an der Wand abgeschlossen sind, kommt alles wieder an seinen Platz. Dann herrscht wieder Ordnung. (Dass die Bilderrahmen in unterschiedlicher Höhe und Orientierung hängen, gehört zu dieser Ordnung ;-)

image

Ohne die zwischenzeitliche Unordnung wäre die Veränderung nicht umsetzbar gewesen. Oder der Aufwand wäre viel, viel höher ausgefallen. Ich denke, da werden Sie mir zustimmen.

Wer einen ordentlichen Zustand in einen anderen, “erweiterten” ordentlichen Zustand überführen will, der erzeugt auf dem Weg dahin Unordnung.

Vorher hat das Wohnzimmer gewisse funktionale und nicht-funktionale Anforderungen erfüllt. Jetzt erfüllt es neue nicht-funktionale Anforderungen. Vorher war es ordentlich, jetzt ist es wieder ordentlich. Alles wunderbar.

Nur bei Quellcode oder anderen Artefakten der Softwareentwicklung soll das anders sein?

Merkwürdig, oder?

Was Unordnung bei Quellcode genau ist, sei mal dahingestellt. Aber dass es Unordnung geben kann, nein, geben muss, sollte außer Zweifel stehen. Auch für einen Laien.

Und genauso sollte außer Zweifel stehen, dass eine Veränderung an Quellcode zu Unordnung führen muss. Zwangsläufig. Immer.

Zumindest halte ich das für absolut einsichtig unter der Voraussetzung, dass Quellcode nach erfolgreichem Abschluss von Veränderungen, ordentlich zurückgelassen wird. Es geht also so mit dem Quellcode:

Anforderungen A+B werden erfüllt –> Veränderungen für C anbringen –> Anforderungen A+B+C werden erfüllt

In Bezug auf die Ordnung sieht das so aus:

Ordnung –> Unordnung –> Ordnung

Wer hat also je überhaupt annehmen können, dass es ohne Clean Code, ohne Refactoring geht? Das war und ist widersinnig. So funktioniert die Veränderung von etwas Ordentlichem nicht.

Wenn professionelle Arbeit in etwas Ordentlichem resultiert – vom Klempner über den Arzt bis zum Softwareentwickler –, dann darf und muss zwischendurch im Verlauf der Arbeit Unordnung entstehen. Und die wird am Ende aufgeräumt. So ist das immer.

Und jetzt nochmal die Frage: Wer glaubt, dass bei der Veränderung von Quellcode zur Erfüllung neuer Anforderungen (oder auch zum Bug Fixing) ohne Unordnung abgehen kann?

Aus meiner Sicht sind das viele Entwickler. Und es sind noch mehr nicht-Entwickler. Der Glaube ist so weit verbreitet, dass Refactoring immer noch einer besonderen Begründung bedarf. Wie oft habe ich gehört, “Für Clean Code haben wir keine Zeit. Wenn uns jemand dabei sieht, wie wir ein Refactoring durchführen, dann haben wir Erklärungsnot.”

Aber das ist Quatsch. Nach dem Bilderleisteneinsatz im Wohnzimmer ist mir das nochmal ganz deutlich geworden.

  1. Professionelle Arbeit hinterlässt ein System in einem ordentlichen Zustand.
  2. Veränderungen an einem ordentlichen System führen zwangsläufig zu Unordnung. Die sollte man gar nicht erst vermeiden wollen.
  3. Um nach Veränderungen wieder ein ordentliches System zu haben, muss aufgeräumt werden.

Unordnung herstellen, um effizient Veränderungen vornehmen zu können – hier: Tisch verrücken, Polster von der Sitzbank nehmen, Werkzeuge bereitlegen – und Ordnung nach Abschluss der Veränderungen wieder herstellen, sind die Klammern professioneller Arbeit.

Ich glaube sogar, dass wir während der Veränderung unseres Quellcodes noch zuwenig Unordnung zulassen. Wir meinen, es ginge ohne. Wir versuchen, mit Überziehern an den Schuhen auf Sitzbankpolstern vorsichtig herumzutreten. Gebohrt wird nicht, denn das macht ja Dreck. Besser nur vorsichtig kleben. Und langsam arbeiten, damit nichts zerbrechliches umgeworfen wird.

Wir versuchen also, Strukturen zu verändern, ohne sie temporär zu “verstören”. Das ist gut gemeint – aber an der Realität effizienter Veränderungsarbeit vorbei, glaube ich.

Nicht nur ist es ganz normal, nach Veränderungsarbeit Ordnung herzustellen; Stichwort Refactoring. Es ist auch ganz normal, vor (!) Veränderungsarbeit Unordnung herzustellen, wenn das einer effizienteren Veränderung dient.

Also, haben Sie kein schlechtes Gewissen, wenn Sie für eine neue Anforderung mal Unordnung erzeugen. Haben Sie auch kein schlechtes Gewissen, hinterher immer aufzuräumen. Das ist normal, unvermeidbar, professionell. Widersetzen Sie sich Anweisungen, die das Gegenteil verlangen. Zur professionellen Arbeit gehört Ordnung im Ergebnis – genauso wie zwischenzeitliche Unordnung.

Mittwoch, 14. August 2013

Vom Nutzen und Nachteil (technischer) Schulden

Gerade habe ich einen Beitrag von Norbert Eder zum Thema “Technische Schulden” (technical debt) gelesen. Er fragt, wer denn schuld sei an den Schulden.

Sich darüber Gedanken zu machen, finde ich nicht falsch. Aber wie bei anderen Artikeln zum Thema erscheint mir die Darstellung leider einseitig. Vielleicht entspricht das der aktuellen Stimmung im Land, die vieles was mit (Geld-)Schulden zu tun hat, sehr skeptisch sieht?

Doch wir sollten uns von diesen großen Problemen mit Schulden nicht ins Bockhorn jagen lassen.

Schulden sind nicht per se schlecht.

Die Möglichkeit, Schulden machen zu können, ist vielmehr die Grundlage für eine entwicklungsfähige Zivilisation.

Was sind Schulden?

Schulden sind eine Vorwegnahme von Möglichkeiten. Eigentlich habe ich heute eine gewisse Möglichkeit nicht - aber indem ich Schulden mache, habe ich sie dann doch.

Beispiele:

Ich kann mir heute kein Auto kaufen, weil ich nicht genug Geld habe? Dann leihe ich mir Geld - ich mache Schulden - und kann mir doch ein Auto kaufen. Später zahle ich das Geld zurück.

Oder: Eigentlich habe ich keine Zeit, spät nachts einen Film zu schauen. Ich müsste schlafen, um morgen fit zu sein. Aber ich schaue doch den Film, nehme also “Zeitschulden” auf, die ich morgen zurückzahle. Entweder Zahle ich sie morgen durch längeren Schlaf zurück. Oder ich zahle sie in Form von weniger Fitness zurück.

Oder: Wenn ich lange gesund leben will, sollte ich z.B. nicht rauchen. Wenn ich es doch tue, dann habe ich jetzt vielleicht mehr Lebensqualität - die ich später aber mit weniger oder gar keinem Leben bezahle.

Solche Ermöglichungen im hier und jetzt haben natürlich ihren Preis. Jetzt Geld, Aufmerksamkeit, Lebensqualität zu haben, kostet in der Zukunft Geld, Aufmerksamkeit, Lebensqualität. Und zwar mehr als ich jetzt bekomme.

Aber das mag es mir ja wert sein. Aus welchen Gründen auch immer.

Wer Schulden macht, handelt ökonomisch.

Schulden eröffnen Chancen

Viele Chancen im Leben erfordern mehr Möglichkeiten als wir gerade haben. Wenn wir sie wahrnehmen wollen, müssen wir Schulden machen.

Beispiele:

Nur wenn ich jetzt ein Auto habe, kann ich einen bestimmten Job bekommen, der mir die Chance auf eine Karriere bietet. Wenn ich keine Schulden machen kann, geht mir die Chance durch die Lappen.

Eigentlich will ich nach Hause gehen, um zu schlafen, damit ich morgen Fit bin im neuen Job. Aber da begegnet mir eine tolle Frau: das ist eine Chance auf schöne weitere Stunden oder gar eine glückliche Partnerschaft. Wenn ich jetzt keine Schulden bei meiner Energie und Aufmerksamkeit machen kann, geht mir diese Chance durch die Lappen.

Während die Rückzahlung von Schulden meist in derselben Währung stattfindet, liegen die Chancen eher in anderen Bereichen. Aus Geld-Schulden wird eine Karriere, aus Energieschulden wird Verliebtheit, aus Nikotinabhängigkeit wird Gemeinschaftsgefühl.

Wer Schulden macht, der sucht sein Glück.

Auf das Maß kommt es an

Alles hat sein bekömmliches Maß, auch Schulden.

Schulden machen zu können, also ökonomische Entscheidungen durch die Verschiebung von Möglichkeiten in Bezug auf sein Glück treffen zu können, das ist grundsätzlich eine gute Sache.

Doch wenn man das unbedacht tut, dann kann man sich auch ins Unglück stürzen. Das passiert, wenn man seine Kapazität zur Rückzahlung von Schulden überschätzt. Nicht bei jeder Schuld ist auch klar, wie hoch ihr Preis ist.

Bei einem Kredit für´s Auto ist der Preis klar, aber die Entwicklung der Karriere vielleicht nicht. Beim Zigarettengenuss ist weder der genaue Preis, noch die Entwicklung der “Rückzahlungsfähigkeit” klar.

Man tut also gut daran, genau hinzuschauen, welche Schulden man wann zu welchem Preis aufnimmt.

Und man tut gut daran, die Rückzahlung zu planen, damit sie einen nicht zur Unzeit erwischt. Gewöhnlich steigt auch der Preis der Schulden, je länger die Rückzahlung aufgeschoben ist. Der Schuldiger fordert sein Recht; die Schuld, die Ermöglichung heute geht auf seine Kosten. Sei das eine Bank oder der eigene Körper.

Schulden erfordern Bewusstsein und Maßhaltung

Technische Schulden

Für technische Schulden gilt natürlich dasselbe wie für alle anderen Schulden auch.

Technische Schulden sind nicht schlecht. Sie machen vielmehr eine Werteabwägung zum Wohle eines größeren Ganzen möglich; sie gehören in den ökonomischen Werkzeugkasten jedes Entscheiders in der Softwareentwicklung.

Wie alle Schulden, sollten auch technische allerdings nur bewusst und in Maßen aufgenommen werden. Der Rückzahlungsplan sollte von Anfang an klar sein.

Wer heute sich durch Entscheidungen für suboptimale technische Qualität die Möglichkeit zu einer früheren Präsentation oder geringen Kosten erkauft - der muss gewahr sein, dass diese Schuld zurückgezahlt werden muss. Wieviel, wann und an wen soll gezahlt werden?

Wer daran nicht denkt, der bekommt immer zum ungünstigsten Zeitpunkt Besuch vom Schuldeneintreiber. Sie kennen das aus vielen Krimis. Und dann ist die Panik groß. Dann beginnt das Bitten und Betteln. Doch der Schuldeneintreiber ist immer gnadenlos. Er mag etwas Aufschub gewähren - doch am Ende muss gezahlt werden. Und zwar niemals weniger als ursprünglich geschuldet.

Also:

Technische Schulden sind nicht böse - solange Sie damit sinnig umgehen.

Aber kennen Sie Ihr Maß. Lassen Sie sich nicht verleiten. Denken Sie vom ersten Tag an die Rückzahlung.

Und vermeiden Sie wie auch sonst im Leben, alte Schulden mit neuen Schulden zu begleichen.

Donnerstag, 23. Mai 2013

Gleich zu Gleich für Clean Code

Letzten Dienstag habe ich im wöchentlichen Unterrichtsblock der Clean Code Developer School (ccd-school.de) “TDD as if you meant it” anhand der Kata WordWrap vorgestellt. Dann wollten die Teilnehmer es selbst probieren. Als Aufgabe habe ich die Kata ToDictionary vorgeschlagen.

Leider führte die dann nicht in so geradliniger Weise zu “Refactoring-Druck”, wie ich es mir erhofft hatte. Es wollten nicht die schönen Wiederholungsmuster wie bei WordWrap auftreten. Mist.

Jetzt habe ich sie selbst nochmal durchgeführt. Hier ein Zwischenstand:

image

Es gibt ein Muster:

  • Deutliche ist es zu sehen im zweiten Test, wo die Aufteilung einer Zuweisung (z.B. “a=1”) in Key und Value (z.B. “a” und “1”) mit anschließendem Eintrag in das Dictionary zweimal geschieht.
  • Weniger deutlich ist es im ersten Test, wo das auch passiert, aber in etwas anderer Form.

Aber was für ein Refactoring-Druck entsteht dadurch? Sollte ich Split()+Add() in eine eigene Methode rausziehen? Dann würde die Erzeugung des Dictionary zurückbleiben. Hm… das fühlt sich nicht gut an.

Ebenfalls unschön ist, dass die Erzeugung des Dictionary im zweiten Test “weit weg” von seiner Nutzung steht. Mir wäre dies lieber:

image

Wenn früher in Sprachen Deklarationen am Anfang eines Unterprogramms stattfinden mussten, dann hatte das weniger mit sauberem Code zu tun, als vielmehr mit der Notwendigkeit zu simpleren Parsern, würde ich sagen. Heute ist das kein Thema mehr. Also können Deklarationen stehen, wo es für das Verständnis sinnvoll ist. Und das, so scheint mir, ist nahe ihres Gebrauchsortes.

Außerdem sollten Deklarationen nur die kleinstmögliche Reichweite/Sichtbarkeit haben. Das beugt unnötiger/zufälliger Kopplung vor.

Dito sollten die Verwendungen von Variablen nahe beieinander stehen. Sie sind ja natürlich kohäsiv. Deshalb würde ich auch die zweite Eintragung an die erste rücken wollen:

image

Selbiges gilt für den Zugriff auf assignments. Also sollten die Aufteilungen der Zuweisungen in Key und Value beieinander stehen:

image

Und schon sieht die Welt viel musterhafter aus, oder? Hier sind Wiederholungen zu sehen, die nach Zusammenfassung schreien. Mit Linq ist das ganz einfach, ohne neue Methoden erzeugen zu müssen:

image

Das ist dann vielleicht noch nicht so gut zu lesen wie eine weitere Kapselung:

image

Doch mit oder ohne eigene Methoden ist die Zusammenfassungen besser zu lesen. Indem ich Gleich und Gleich sich habe gesellen lassen, statt dem ersten Impuls nach Auslagerung eines Musters zu folgen, ist die Sauberkeit noch größer geworden, würde ich sagen. Nun sind die wesentlichen Aspekte der Problemlösung deutlich zu sehen. Es ist klar, wie die Lösung voranschreitet. Insbesondere bei weiterer Kapselung der Aspekte in Methoden ist ToDictionary() sehr leicht zu lesen.

Sonntag, 28. April 2013

Software fraktal - Funktionale Abhängigkeit entschärfen

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

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

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

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

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

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

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

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

Oder?

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

Operationen

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

Die Funktion

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

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

Operationenhierarchien

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

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

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

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

...

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

Operationenhierarchien leiden deshalb unter einigen Problemen:

Problem #A: Grenzenloses Wachstum

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

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

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

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

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

Problem #C: Attrappen auf jeder Ebene

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

Integration

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

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

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

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

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

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

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

Trennung von Integration und Operation

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Skalieren mit dem IOSP

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


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

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

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

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


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



Fazit

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

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

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

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

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

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

Endnoten

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

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

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

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

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

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

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

Mittwoch, 24. April 2013

Raus aus der funktionalen Abhängigkeit

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

Wir sollten einfach keine funktionalen Abhängigkeiten mehr eingehen.

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

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

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

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

Func<int,int,int>

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

Anders ist das bei einer Funktion wie dieser:

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

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

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

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

Aufbrechen funktionaler Abhängigkeiten

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

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

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

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

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

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

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

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

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

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

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

42 |> c_vorbereitung |> s |> c_nachbereitung

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

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

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

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

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

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

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

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

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

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

Vorteile der Vermeidung funktionaler Abhängigkeiten

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

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

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

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

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

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

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

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

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

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

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

Fazit

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

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

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

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

Endnoten

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

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

Sonntag, 21. April 2013

Warnung vor der funktionalen Abhängigkeit

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

Funktionale Abhängigkeit

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

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

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

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

Problem #1: Bereitstellungsaufwand

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

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

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

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

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

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

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

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

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

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

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

Problem #2: Topologische Kopplung

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

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

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

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

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

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

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

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

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

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

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

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

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

Problem #3: Grenzenloses Wachstum

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

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

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

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

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

So einfach ist das.

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

Ja, so einfach ist das, glaube ich.

Alles beginnt mit etwas Logik in c():

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

Dann kommt eine funktionale Abhängigkeit dazu:

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

Und dann kommt noch eine dazu:

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

Und dann kommt noch eine dazu:

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

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

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

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

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

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

Problem #4: Syntaktische Kopplung

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

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

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

Problem #5: Semantische Kopplung

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

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

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

Fazit

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

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

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

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

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

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

Mittwoch, 3. April 2013

Clean Code Development anders lernen

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

image

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

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

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

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

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

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

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

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

Deshalb machen wir es jetzt anders.

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

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

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

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

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

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

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