Notorische Aspektlosigkeit

Immer wieder, wenn ich Resultate von Coding Dojos sehe, beschleicht mich ein ungutes Gefühl. Irgendetwas fehlt mir bei den Lösungen. Dabei ist es egal, ob das Coding Dojo in der Gruppe durchgeführt wurde oder sich jemand allein zum Einzeltraining ins Dojo begeben hat.

Anlässlich eines kundeninternen Coding Dojos habe ich nun nochmal länger über die gefühlte Lücke nachgedacht. Als Aufgabe wurde die Kata Word Wrap bearbeitet. Der Abend war gut vorbereitet: Laptop, Beamer, Catering, Aufgabenbeschreibung. Und sogar Testfälle waren hübsch priorisiert schon notiert, damit sich die Gruppe auf die TDD-Fingerfertigkeit konzentrieren konnte [1].

Nach knapp 90 Minuten war denn auch die Lösung fast wie aus dem Buch erarbeitet. Alles war harmonisch verlaufen. Kein Haareziehen, keine Weigerungen, mal nach vorn zu gehen und im Pair zu codieren. Alles hätte also gut sein können.

Allein… der Code sah so aus:

image

oder vielleicht auch so:

image

oder er könnte auch so ausgesehen haben:

image

Letztlich ist es egal, wie genau er ausgesehen hat. Ich habe ihn nicht mitgeschrieben, die Beispiele oben sind aus den Weiten des Internet. Alle haben aber das gemeinsam, was ich als fehlend empfunden habe.

Mir fehlen die Aspekte.

Coding Dojo Lösungen sind nach meiner Erfahrung notorisch aspektlos. Oder anders ausgedrückt: die Prinzipien SRP und SoC finden selten Anwendung. SLA kommt allerdings hin und wieder vor.

Worüber ich immer wieder gestolpert bin ist also die Unverständlichkeit der Lösungen. Wie funktioniert denn so ein Word Wrap. Schauen Sie sich einfach das erste Listing an und formulieren Sie in einem ganzen Satz, wie (!) das geht mit dem Wortumbruch.

Ich bezweifle nicht, dass die Lösungen funktionieren. So unterschiedlich sie aussehen, die kanonischen Testfälle absolvieren sie fehlerfrei. Aber das ist ja seit 50 Jahren eher nicht das Problem in der Softwareentwicklung. Wir können lauffähige Software herstellen. Und auch noch solche, die performant, skalierbar, sicher usw. ist.

Dass in der Eile da mal ein paar Bugs durch die Maschen schlüpfen… Das ist nicht schön, kriegen wir aber in den Griff, wenn wir mehr automatisiert testen. TDD hilft auch.

Aber ist der nützliche Code, den wir da produzieren auch verständlich?

Eher nicht. Und das beginnt bei so kleinen Beispielen wie den obigen.

Vielleicht stelle ich mich jetzt dumm an? Vielleicht schade ich geradezu meinem Ansehen, wenn ich meine Begriffsstutzigkeit so deutlich dokumentiere? Keine Ahnung. Ist mir auch egal. Fakt ist, dass ich es schwierig finde, aus den obigen Beispielen herauszulesen wie (!) so ein Wortumbruch funktioniert.

Vielleicht schauen Sie ja einfach drauf und sagen nach 10 Sekunden: “Achsooo! So geht das. Klar. Hätte ich auch so gemacht.” Kann sein. Dann beneide ich Sie. Ich würde das auch gern können.

Bitte verstehen Sie mich richtig: Ich kann natürlich den Code lesen und herausfinden, wie er funktioniert. Ich kann auch selbst eine Lösung herstellen.

Mir geht es aber nicht darum, ob ich irgendwie zu einem Codeverständnis gelangen kann, sondern wie schnell.

Ich habe den vielleicht verwegen klingenden Anspruch, dass sich im Code die Domäne widerspiegelt. Dass oben drüber Wrap() steht, ist mir nicht genug. Dadurch weiß ich nur, worum es irgendwie geht. Das hilft mir wenig, wenn ich den Code ändern soll, weil ein Bug drin steckt oder sich die Anforderungen geändert haben.

Sorry to say, die obigen Lösungen sind im Wesentlichen alle Monolithen. Kleine Steine, aber immer noch ziemlich strukturlose Steine. Nur Uncle Bob hat es etwas besser gemacht. Aber dazu gleich mehr.

Ich hab es einfach satt, im Steinbruch zu arbeiten. Ich will nicht mehr Archäologie betreiben und darüber rätseln, was eine Entwicklerintelligenz irgendwann mal ersonnen hat, das ich mir nun mühsam wieder erarbeiten muss. Ich will nicht bei jedem 20-Zeiler Reverseengineering betreiben. Das ist einfach teuer. Das kostet wertvolle Lebenszeit. “Was will uns der Autor sagen?” ist eine Frage für das Feuilleton oder den Deutschunterricht.

Jeder genügend obskure Code erscheint magisch ;-) (frei nach Arthur C. Clark)

Was ist stattdessen will, ist eine irgendwie lesbare Lösungsbeschreibung – im Code. Ja, genau, im Code. Mir geht es nicht um Papierdokumentationsberge und auch nicht um mehr Kommentare. Ist schon so, im Code steckt die ultimative Wahrheit. Aber wenn das so ist, dann doch bitteschön etwas leichter erkennbar als bei den Upanishaden oder der Kabbala.

Also, wie funktioniert ein Word Wrap? Keine Ahnung, wie die obigen Autoren meinen, dass es funktioniert. Aber hier mal ein Vorschlag:

  1. Der Input-Text wird in Worte zerlegt.
  2. Die Worte werden in Silben zerlegt.
  3. Die Zeilen des Output-Textes werden aus Silben zusammengesetzt, bis die maximale Zeilenlänge erreicht ist.

Das ist mein Vorschlag für eine Lösung auf hohem Abstraktionsniveau, sozusagen ein Entwurf. Ich habe das Problem in drei Aspekte zerlegt: Wortermittlung, Silbenbestimmung, Zeilenzusammenbau. Der Entwurf folgt damit meiner Interpretation der Aufgabenstellung, die da lautet “KataWordWrap”.

  1. Ich nehme den Wortumbruch ernst. Deshalb taucht in meiner Lösungsskizze der Begriff “Wort” auf.
  2. Die Silben sind meine Hinzudichtung. Die stehen nicht in der Aufgabenstellung. Mit ihnen meine ich auch nicht un-be-dingt Sil-ben, sondern Abschnitte von Worten, zwischen denen getrennt werden darf. In der Problemstellung ist das jeder Buchstabe. Den Begriff “Silbe” habe ich hinzugedichtet, um das Problem an etwas Bekanntes anzuschließen.

Mit dieser Lösungsidee im Hinterkopf schaue ich nun auf den obigen Code und sehe… nichts. Dort tauchen die Begriffe “Wort” oder “Silbe” nicht auf. Und Lösungsschritte sehe ich auch nicht.

Allerdings macht Uncle Bob eine Ausnahme. Dessen Code liefert einen gewissen Hinweis auf seinen Lösungsansatz. Allerdings lese ich auch da nichts von “Wort” oder “Silbe”, sondern von “Zeilenbruch”. Hm… das ist interessant.

Meine Interpretation: Uncle Bob löst ein anderes Problem als ich ;-) Uncle Bob löst das Problem “Zeilenlängenbegrenzung” (oder so ähnlich) und nicht das Problem “Wortumbruch”. Beides mag ähnlich klingen, ist aber etwas anderes.

Wer hat nun Recht? Im Ergebnis würden wir wohl dasselbe liefern – aber unsere Lösungen würden sich eben unterscheiden. Wir sehen unterschiedliche Domänen.

Aber auch wenn Uncle Bob eher die Domäne im Code repräsentiert, wie er sie aus der Aufgabenstellung herausgelesen hat, finde ich seinen Lösungsansatz noch nicht leicht zu lesen. Was bedeutet zum Beispiel if (s.charAt(col) == ‘ ‘)?

Klar, dass kann ich mir irgendwie zusammenreimen. Aber ich will mir nichts mehr zusammenreimen. Ich habe mal Informatik studiert und nicht Ethnologie oder Archäologie. Wenn der Code nicht intention revealing ist, dann habe ich damit ein Problem.

Code muss ich lesen und verstehen, nicht wenn und weil er läuft, sondern wenn er eben nicht läuft wie gewünscht, d.h. wenn ich an ihm etwas verändern soll. Dann muss ich verstehen, wie er seine Aufgabe heute erledigt und leicht die Ansatzpunkte identifizieren, wo ich Veränderungen im Sinne neuer Anforderungen anbringen muss.

Was könnten denn Veränderungen sein, die an “Word Wrap” Code herangetragen werden?

  • Die maximale Output-Zeilenlänge ändert sich. Es verändert sich nur eine Konstante.
  • Der umzubrechende Text liegt nicht in einer Zeile, sondern in mehreren vor. Es verändert sich also die Struktur der Quelldaten.
  • Der Quelltext hat mehrere Absätze, die im Output erhalten bleiben sollen. Eine Veränderung nicht nur in der Quelldatenstruktur, sondern auch in der Zieldatenstruktur.
  • Worte sollen nicht mehr an beliebiger Stelle geteilt werden dürfen, sondern nur noch zwischen Silben.
  • Beim Umbruch sind Satzzeichen zu berücksichtigen. So darf zum Beispiel ein Bindestrich nicht am Anfang einer neuen Zeile stehen.

Ich behaupte nun nicht, dass eine Kata-Lösung all dies und mehr vorhersehen sollte. Keinesfalls! Die Lösung soll ausschließlich den aktuellen Anforderungen genügen. Auch wenn meine Lösungsskizze oben Silben erwähnt, würde ich also keine echte Silbenerkennung implementieren, sondern den Umbruch bei jedem Buchstaben zulassen.

Dennoch ist ja eines immer wieder so überraschend wie Weihnachten: dass sich Software über die Zeit ändert.

Deshalb sollten wir Vorkehrungen treffen, damit uns das leicht fällt.

Für mich bestehen solche Vorkehrungen mindestens darin, die Aspekte des Lösungsansatzes klar im Code sichtbar zu machen. Und genau das geschieht eben nicht in den ersten beiden obigen Lösungen und nur bedingt bei Uncle Bob.

Wir können Aspekte in Methoden oder Klassen verpacken. Je nach Umfang. Das ist doch kein Hexenwerk. Und wir können auch noch die fundamentalen Aspekte Integration und Operation trennen.

Dann würden unsere Lösungen lesbarer, glaube ich.

TDD is not enough

Noch ein Wort zur Ursache der Unlesbarkeit. TDD ist da sicherlich nicht schuld. TDD ist ne gute Sache. Soll man im Coding Dojo gerne üben.

Aber man sollte eben die Heilsbringererwartungen nicht zu hoch schrauben. TDD dreht sich am Ende doch “nur” ums Testen. Auch wenn es red – green – refactor heißt, so führt das eben nicht automatisch zu lesbarem, evolvierbarem Code.

Eine gut priorisierte Liste von Testfällen plus red – green liefert Code, der in kleinen Schritten hin zu einer funktionalen Lösung wächst. Die Codeabdeckung wird groß sein. Das halte ich für das erwartbare Ergebnis, sozusagen das kleinste garantierte Resultat von TDD.

Der Rest jedoch… der hat eben nichts mit TDD zu tun. Verständlichkeit, Evolvierbarkeit, punktgenaues Testen (also Unit Tests statt Integrationstests [2])… all das ist optional bei TDD. Es entsteht nicht zwingend. Das ist meine Erfahrung aus vielen Coding Dojos und zeigt sich auch an den obigen Codebeispielen.

Damit behaupte ich nicht, dass das nie entstehen würde. Ich sage nur, das entsteht nicht durch TDD, sondern durch Refactoring-Mühe und –Kompetenz.

Refactoring mit TDD gleichzusetzen, wäre jedoch falsch. Refactoring ist eine unabhängige Disziplin. Deshalb sage ich, Verständlichkeit und Evolvierbarkeit entstehen eben nicht durch TDD. Und wenn man in einem Coding Dojo TDD übt, dann übt man de facto eben eher nicht Refactoring. Das wäre auch eine Überlastung für die Beteiligten, ein Verstoß gegen das SRP, sozusagen. Mit der Definition von Tests, mit dem red-green-Rhythmus, mit KISS bei der Implementation, mit all dem haben die Beteiligten schon genug Mühe. Wenn Refactoring da nicht quasi im Stammhirn verankert ist, also schon als Reflex existiert, dann findet Refactoring nur auf Alibiebene statt, da wo es einem schlicht ins Auge springt, weil so deutliche Anti-Muster im Code zu sehen sind.

Was dabei aber eher nicht entsteht, das ist Aspekttrennung. Und schon gar nicht entstehen getrennte Tests für Aspekte.

Indem ich das so “anprangere” will ich nicht die Praxis der Coding Dojos herabsetzen. Mir geht es nur um realistischere Erwartungen. Die Ergebnisse sind schlicht und einfach Ergebnisse der grundlegenden Wahrheit: you always get what you measure. Denn was bei TDD “gemessen” wird, worauf das Augenmerk liegt, das sind die Tests. Es heißt ja “Test-Driven Development” und nicht “Design-by Testing” oder so. Tests stehen im Vordergrund und im Mittelpunkt. Und die kriegt man. Alle sind zufrieden, wenn sie im red-green-Rhythmus arbeiten und schrittweise die Lösung hinkriegen. Und auf dem nächsten Level dann noch bei jedem green in ein VCS einchecken.

Das ist alles super. Nur eben auch nicht mehr. Es kommen Lösungen heraus, die testabgedeckt sind. Wunderbar. Man merkt ihnen an, dass sie stolz geschrieben wurden. Ans Lesen und spätere Verändern wurde dabei wenig Gedanke verschwendet. Während des Coding Dojos ist ja der ganze Kontext auch in allen Köpfen, die Lösung entsteht vor aller Augen. Niemand hat ein Problem mit der Verständlichkeit. Noch nicht.

Deshalb erhärtet sich bei mir weiter der Verdacht: TDD is not enough. Wenn wir das sehr löbliche Üben auf TDD beschränken, dann tun wir zuwenig.

Dass es darauf beschränkt ist, finde ich ja verständlich. red – green + KISS ist so schön greifbar. Da sieht man den Erfolg. Und KISS ist ganz im Sinne des alten Eleganzgedankens.

Leider sind das Ergebnis dann lokale Optima. Knapper testabgedeckter funktionaler Code ist im Sinne dieses Vorgehens optimiert – aber hat nicht das Big Picture im Blick. Zu dem gehören für mich Verständlichkeit und Evolvierbarkeit.

In diesem Sinne ist auch die Freude aller zu verstehen, wenn die Lösung kleiner wird. Weniger LOC sind gut. Als sei darauf ein Preis ausgelobt.

Es gilt jedoch nicht Verständlichkeit == Kürze. Verständlichkeit mag mit Kürze korrelieren, deshalb ist jedoch der kürzeste Code nicht automatisch der verständlichste. Für mich darf also etwas “Fluff” in einer Lösung sein, gar etwas Redundanz, wenn dadurch Verständlichkeit und Evolvierbarkeit steigen.

Versuchen Sie es doch mal. Einfach so als Experiment. Beim nächsten Coding Dojo könnten Sie sich bewusst mal die Frage stellen, welche Aspekte Sie erkennen – und dann überlegen Sie, wie Sie die in der Lösung deutlich sichtbar machen. Darauf aufbauen könnten Sie nach einer Formulierung der Lösung suchen, die ihre Funktionsweise leicht erkennbar macht. Wie arbeiten die Aspekte einer Lösung zusammen, um die Anforderungen zu erfüllen?

Fußnoten

[1] Dass Testfälle schon ausgearbeitet vorlagen, fand ich allerdings nicht so schön. Das schien mir dem Lerneffekt eher abträglich. Denn darin liegt ja eine der zentralen Herausforderungen bei TDD: relevante Testfälle überhaupt erst einmal erkennen und dann noch in eine Reihenfolge bringen, die ein kleinschrittiges Wachstum des Codes begünstigt.

[2] Selbst bei Uncle Bob gibt es nur Tests für wrap(). Die herausgelöste Funktion breakline() wird nicht separat getestet, obwohl sie ein Arbeitspferd der Lösung darstellt. Damit sind alle Tests seiner Lösung Integrationstests – auch wenn sie auf einem höheren Abstraktionsniveau die Unit wrap() testen.


wallpaper-1019588
LUCK LIFE: Band feiert Europapremiere auf der Connichi
wallpaper-1019588
Wind Breaker: Deutscher Simuldub bei Crunchyroll gestartet
wallpaper-1019588
Kizuna no Allele: Erste Staffel erscheint auf Disc
wallpaper-1019588
Acro Trip: Promo-Video verrät Startzeitraum