Aspektvolles Programmieren

Erstellt am 12. November 2012 von Ralfwestphal @ralfw

Neulich habe ich kritisiert, Lösungen für Code Katas seien oft schwer verständlich. Als Ursache dafür habe ich einen Mangel an herausgearbeiteten Aspekten identifiziert.

Mit Code Katas wird üblicherweise TDD geübt. Das hatte Dave Thomas zwar nicht speziell im Sinn, als er zu Code Katas ermunterte, doch es ist die de facto Praxis in Coding Dojos. Explizit hat Dave Thomas gesagt:

“Some involve programming, and can be coded in many different ways. Some are open ended, and involve thinking about the issues behind programming.” (meine Hervorhebung)

Dass über die Themen hinter der Programmierung nach-ge-dacht würde… Nein, tut mir leid, das habe ich in keinem Coding Dojo bisher erlebt, weder in einem offenen, noch in einem inhouse Dojo. Der Goldstandard ist vielmehr, dass schon Rechner und Beamer eingerichtet sind, kurz das Organisatorische besprochen wird, man sich flux über die Aufgabe austauscht – max. 15 Minuten – vor allem aber zügig das (Pair) Programming beginnt. Und damit endet dann ein eventueller “Nachdenkmodus” endgültig.

Klar, es gibt Diskussionen über die nächsten Schritte. Aber so richtig ins Nachdenken über “issues behind programming” kommt da niemand mehr. Ist ja auch nicht das ausgelobte Ziel. Das lautet vielmehr, die Lösung möglichst weit mit TDD voran zu bringen. [1]

Und so sehen dann die Lösungen aus. Man bekommt, was die Regeln vorgeben: grüne Tests. Das ist nämlich das einzige, was irgendwie “gemessen” wird. Darüber gibt es keinen Zweifel. Schon die Qualität der Tests ist nicht mehr leicht zu ermitteln. Sie ergibt sich eher implizit dadurch, ob der Fortschritt leicht oder schwierig ist. Denn sind die Tests schlecht geschnitten und ungünstig priorisiert, wird das Codieren zäh.

Aber wie denn anders?

Nun, es kommt erstmal aufs Ziel an. Was soll denn mit einer Code Kata erreicht werden? Wenn das ausschließlich lautet “TDD-Fingerfertigkeit entwickeln”, dann ist ja alles gut. Weitermachen mit Dojos wie bisher.

Nur finde ich das inzwischen erstens langweilig und zweitens am Hauptproblem vorbei. Langweilig, weil nach 5+ Jahren Coding Dojos auch mal was anderes dran sein dürfte. Oder findet das sonst niemand zum einschlafen, wenn erwachsene Männer womöglich Monat für Monat zusammenkommen, um diese Fingerfertigkeit zu entwickeln. Mit Verlaub: Das ist doch viel, viel simpler, als aus einer Karate Kata das Letzte herauszuholen.

Vor allem löst diese spezielle Variante automatisierter Tests nicht das Hauptproblem der Codeproduktion. Das ist mangelhafte Evolvierbarkeit von Code.

Nicht, dass es keine Teams gäbe, die nicht von automatisierten Tests profitieren könnten. Davon gibt es noch viele. Die sollen da auch möglichst schnell Fuß fassen. Nur ist deren Einführung erstens technisch kein Hexenwerk: NUnit runterladen, referenzieren, los geht´s. Und zweitens ist die konkrete Praxis automatisierter Tests relativ unbedeutend. Ob nur “irgendwie” test-first oder richtiggehendes TDD oder Tests nach der Implementation geschrieben… klar, die Ergebnisse sind unterschiedlich, doch entscheidend ist nicht dieser Unterschied durch die Stile, sondern der zur vorherigen Testlosigkeit.

Warum fällt es denn aber Teams auch heute noch schwer, überhaupt automatisierte Tests einzusetzen? Weil sie keine TDD-Fingerfertigkeit haben? Nein, weil sie keine Codebasis haben, die das (nachträgliche) Anbringen von Tests erlaubt. Und das kommt daher, dass sie nur wenig Vorstellung davon haben, wie eine saubere, testbare Codebasis überhaupt aussieht.

Damit sind wir beim blinden Fleck der TDD-Coding-Dojo-Praxis. Über dieses “issue behind programming” wird nicht nachgedacht. Das Refactoring im TDD-3-Schritt red-green-refactoring ist für das übliche Coding Dojo eine Black Box. Deren Deckel wird höchstens ein wenig angehoben. Besser aber man lässt sie geschlossen. Es könnte eine Büchse der Pandora sein. Wenn die erstmal geöffnet ist, käme man mit dem Codieren ja gar nicht mehr voran – oder würde gar nicht erst beginnen.

Das halte ich für sehr bedauernswert. Das möchte ich ändern.

TDD ist gut und schön. Wem das noch nicht langweilig ist, wer glaubt, dass alles besser wird, wenn er darin nur perfekt ist… der soll gern TDD hoch und runter üben. Mir käme das allerdings so vor, als würde jemand meinen, ein großer Kung Fu Meister zu werden, weil er den ganzen Tag am Mu ren zhuang arbeitet.

Zur Programmierkunst gehört aber doch mehr. Wenn nicht, wäre sie armselig und könnte schon bald von jedermann ausgeübt werden.

Was das ist, das mehr dazugehört? Damit meine ich nicht die unendliche Vielfalt an Technologien. Die ist sicherlich eine große Herausforderung. Doch letztlich ist das eher eine Frage der Masse. Das sind komplizierte Oberflächlichkeiten.

Viel, viel schwieriger sind jedoch die “issues behind programming”. Da geht es um Paradigmen. Da geht es um Methoden. Da geht es um eine Vielzahl von Qualitäten über die “simple” Korrektheit von Code hinaus.

TDD adressiert die vermeintlich. Aber in Wirklichkeit adressiert sie nicht TDD selbst, sondern das Refactoring, auf das sich TDD stützt. Nur wird eben genau das nicht geübt. [2]

Nun aber genug mit dem TDD bzw. Coding Dojo Bashing ;-) Ich will mich ja auch nicht zu sehr wiederholen.

Wie stelle ich es mir denn nun anders vor? Wie hätte ich mir ein Vorgehen zum Beispiel in Bezug auf die Kata Word Wrap gewünscht?

1. Domäne spezifizieren

Als erstes sollte gefragt werden, was denn wirklich, wirklich die Problemdomäne ist. Worum geht es? Und das sollte dann in einem Satz knackig formuliert werden.

Schon bei der Kata Word Wrap ist das nicht ganz einfach. Ich habe mich da auch erst verlaufen. Der Titel ist nämlich fehlleitend. Er passt nicht zur Aufgabenstellung.

Im Titel kommt Word Wrap vor. Damit scheinen Worte und ihr Umbruch im Kern der Ubiquitous Language zu stehen. Interessanterweise tauchen dann Worte in der Aufgabenbeschreibung eigentlich nicht mehr auf. Da steht zwar “You try to break lines at word boundaries.” – doch zentral ist nicht “word”, sondern “break lines”.

Es geht nicht um Word Wrap, sondern um Zeilen(um)bruch. Das mag zunächst im Gespräch ein feiner Unterschied sein – doch in der Implementation würde der sich ausweiten.

Ich habe mich auch verleiten lassen, eine Wortumbruchlösung zu suchen, statt einer für den Zeilenumbruch. Doch der geht an der Aufgabe vorbei. [3]

Mein Vorschlag für eine knackige Beschreibung der Problemdomäne wäre dann zum Beispiel diese:

Der Wrapper zerlegt einen einzeiligen Text in mehrere Zeilen einer Maximallänge zerlegen - wobei Worte nicht zerschnitten werden sollen, wenn es sich vermeiden lässt.

Diese Beschreibung betont die Zeilenorientierung. Worte kommen darin nur insofern vor, als dass sie nicht “beschädigt werden sollen”. Die Zerlegung soll zu keinen “Kollateralschäden” führen.

Ob diese Formulierung die beste ist, sei dahingestellt. Aber sie ist besser als keine. Die Kraft überhaupt einer knackigen Formulierung ist nicht zu unterschätzen. Sie richtet das weitere Denken aus, sie spannt einen Lösungsraum auf – und zieht damit gleichzeitig eine Grenze. So kann konstruktive Diskussion über einen Lösungsansatz entstehen. Denn die kann, um sich fokussiert zu halten, immer wieder auf diese knappe Spezifikation verweisen. Der Zweck ist also ähnlich der der Metapher im XP.

2. Lösung skizzieren

Mit der Beschreibung der Problemdomäne als Leitstern und der Anforderungsdefinition als Karte, sollte als nächstes ein Lösungsansatz erarbeitet werden. Das geschieht im Wesentlichen durch Nachdenken. [4]

Die Frage lautet: Wie würde ich das Problem lösen, wenn ich es von Hand tun müsste?

Bei der Beantwortung soll natürlich jede Art von Erfahrung eingebracht werden. Alles ist erlaubt. Das Ergebnis kann ebenfalls alles mögliche sein – nur kein Produktionscode.

Früher gab es für algorithmische Aufgabenstellungen die ehrenhaften Mittel Flowchart, Structogramm und vor allem Pseudocode. Heute scheint das nicht mehr hip zu sein. Überhaupt sehen Lösungsskizzen seltsam holprig aus, wo sie denn überhaupt externalisiert werden können. Meist existieren sie ja nur sehr diffus in den Köpfen der Entwickler – und niemand weiß so recht, ob alle denselben Lösungsansatz meinen.

Ich sehe da ein großes Defizit bei unserer Zunft. Die Kunst, Lösungen zu beschreiben, ohne sie gleich zu codieren, ist sehr schwach ausgeprägt.

Dabei verlange ich nichts Spezielles. Mir ist es im Grunde egal, wie man einen Lösungsansatz beschreibt. Er soll nur auf einem höheren Abstraktionsniveau als die spätere Implementierung liegen. Sonst hat man ja nichts gewonnen. Nur über “Skizzen” lässt sich effizient und effektiv diskutieren; nur sie lassen sich in einer Gruppe gemeinsam entwickeln. Code selbst ist dagegen zäh. Man verliert sich auch zu schnell in technischen Details und verliert das Big Picture der Lösung aus dem Blick.

Für die Kata Word Wrap könnte die Lösungsskizze zum Beispiel in einer Schrittfolge bestehen. Nix Grafik, nix Tool, nix großer Aufwand. Einfach nur mal hinschreiben, wie man schrittweise das Problem eines Zeilenumbruchs lösen könnte. Beispiel:

    1. Vom gegebenen Text bricht man eine Zeile von Maximallänge vorne ab. Es entstehen eine Zeile und ein Resttext.
    2. Dann schaut man, ob diese Zerlegung dazu geführt hat, dass ein Wort zerschnitten wurde. Falls ja, macht man die rückgängig: Es wird der abgeschnittene “Wortkopf” am Ende der Zeile zurück an den Anfang des Resttextes zum “Wortrumpf” versetzt.
    3. Die Zeile kann jetzt an den bisher schon erzeugten umgebrochenen Text angefügt werden.
    4. Sofern noch Resttext übrig ist, den als neuen gegebenen Text ansehen und damit genauso wie bis hierher verfahren.

Diese Lösungsschritte zu finden, ist doch kein Geniestreich, oder? Aber nun kann jeder im Team prüfen, ob er versteht, was die Aufgabe ist und wie eine Lösung aussehen könnte. Es kann Einigkeit im Team hergestellt werden. Ein Ziel kann anvisiert werden.

Und wer diese Lösung nicht gut genug findet, der kann jetzt aufstehen und eine Diskussion anzetteln. Gut so! Das ist nämlich auf dieser konzeptionellen Ebene viel einfacher, als wenn erst Berge an Code produziert sind.

Sobald die IDE angeworfen ist, sind alle im Codierungsbewusstseinszustand. Dann ist Diskussion über Grundsätzliches nicht erwünscht. Das finde ich auch total verständlich. Denn Lösungsfindung und Codierung der Lösung sind zwei ganz unterschiedliche Aspekte von Softwareentwicklung. [5]

Nicht nur bei Code ist es also klug, Aspekte zu trennen, sondern auch im Entwicklungsprozess.

3. Lösungsaspekte identifizieren

Aber erstmal zu den Aspekten der Lösung. Wie lauten die? Wie umfangreich sind sie? Wie stehen sie im Zusammenhang?

Im vorliegenden Beispiel sind die Aspekte:

  • Textzerlegung und
  • Textzusammenbau.

Zur Zerlegung gehört das Abschneiden einer Zeile vom gegebenen Text und die eventuell nötige Korrektur dieses Schnitts. Der Textzusammenbau besteht dann nur noch aus dem Anhängen der endgültigen Zeilen an einander.

Durch die Identifikation der Aspekte wird die Lösung nochmal auf ein höheres Abstraktionsniveau gehoben. Hier kann das Team wieder prüfen, ob sie sich stimmig anfühlt. Ja, es geht ums Gefühl, um Intuition, um Erfahrung. Denn bisher ist kein Code geschrieben.

Aber das macht nichts. Entwickler geben ja immer soviel auf ihre Erfahrung. Dann ist hier der richtige Zeitpunkt, sie mal einzusetzen.

Dass diese “Gedankenblasen” nicht crashen… Klar, das ist so. But it´s not a bug, it´s a feature. Würden sie crashen, wären sie Code. Und Code ist vergleichsweise starr – egal ob mit TDD oder sonstwie entwickelt.

Nachdenken, eine konzeptionelle Lösung finden, ist erstens eine Tätigkeit, die das Team zusammenführt. Und zweitens ist es eine qualitätssichernde Maßnahme. Es wird damit nämlich die Qualität des Input für den Engpass “Implementierung” im Softwareentwicklungsprozess angehoben.

Codieren ist kein Selbstzweck. Also sollten wir nicht möglichst viel Code, sondern möglichst wenig Code schreiben. Damit meine ich aber nicht möglichst wenige LOC, sondern möglichst wenige Überarbeitungen. Sozusagen “LOC in place” vs “LOC over time”.

Hier der Überblick über die Aspekte und ihre Zusammenhänge:

So ein Bild kann man leicht kommunizieren. Es enthält die Essenz der verbalen Lösungsskizze. Und es zeigt deutlich die Aspekte. Wenn jetzt zum Beispiel ein neues Teammitglied an Bord käme, könnte es anhand dieses Bildes leicht in den Lösungsansatz eingeführt werden. Warum den also nicht dem Code beigeben? Oder versuchen, ihn im Code möglichst nachvollziehbar festzuhalten?

Ja, genau, dieser Entwurf sollte sich im Code widerspiegeln. Das ist nicht umsonst ein Clean Code Developer Prinzip. Denn wenn der Entwurf nur für den einmaligen Codierungsgebrauch gemacht würde und danach verschwände, müssten Entwickler in Zukunft wieder Codearchäologie betreiben, um herauszufinden, wie das denn alles gemeint war.

Zu sehen ist im Bild eine Aspekthierarchie. Zuoberst der umfassende Aspekt der Domäne, d.h. der Gesamtheit der Anforderungen.

Darunter die beiden aus der Lösungsskizze abstrahierten Aspekte.

Und auf der untersten Ebene deren Sub-Aspekte, die konkreten Operationen. Die Doppelpfeile deuten an, wie sie horizontal zusammenhängen, um die funktionalen Anforderungen zu erfüllen.

Das ist doch nicht schwer zu verstehen, oder? Da muss man kein UML-Studium absolviert haben. Man muss auch nicht OOP- oder FP-Jünger sein. Gesunder Menschenverstand reicht aus. Eine umfassende Aufgabe wurde hier nachvollziehbar zerlegt in kleinere Aufgaben. Das ist ein probater Ansatz bei einer so algorithmischen Aufgabenstellung.

4. Lösung codieren

Mit der Lösungsskizze in der Hand, kann man sich ans Codieren machen. Große Überraschungen sollten sich bei der geringen Komplexität nicht einstellen. Das Codieren kann geradlinig ablaufen. Ob mit TDD oder ohne? Ich sag mal: Zuerst einen oder mehrere Tests für eine Operation zu definieren, bevor man sie implementiert, ist schon eine gute Sache. Das kleinschrittige Vorgehen wie bei TDD jedoch, halte ich in diesem Fall nicht für zwingend. Also: Test-First, ja; TDD, nein.

Und mit welcher Funktionseinheit beginnen?

Das Schöne an der Lösungsskizze ist, dass man beliebig reingreifen kann. Man kann zum Beispiel mit dem Anfügen einer Zeile an den schon umgebrochenen Text beginnen. Hier die Tests zuerst:

Und hier die Implementation:

Das ist überschaubar. Das bringt schnell ein Erfolgserlebnis. Das lässt sich auch sofort in eine Wrap()-Funktion einsetzen…

um erste Integrationstests zu erfüllen:

Ist ja nicht so, dass bei diesem Vorgehen nicht in Durchstichen, d.h. in Nutzeninkrementen gedacht werden soll.

Aber warum nicht den Code direkt in Wrap() schreiben und später raus-refaktorisieren? Weil wir uns auch nicht dümmer stellen müssen, als wir sind. Wir wissen schon, dass das ein eigener Aspekt ist und andere hinzukommen werden. Und Aspekte kapselt man mindestens in eigene Methoden.

Der aspekteigene Test ist ein Unit Test. Wenn später mal etwas schiefgehen sollte, dann zeigen solche Unit Tests viel besser als Integrationstests, wo das Problem liegt. Der Test von Wrap() hingegen ist ein Integrationstest, weil er viele Aspekte auf einmal prüft.

Und so geht es dann weiter: Jeder Aspekt kann für sich codiert werden. Naja, jede Operation, also die Aspekte auf der untersten Ebene bei der obigen Zeichnung. Die darüber liegenden Aspekte integrieren diese, sie sind abhängig. Deshalb ist wohl tendenziell eine bottom-up Codierung angezeigt.

In jedem Fall entstehen Operationsmethoden, die jede für sich getestet sind. Wenn Sie die mit TDD implementieren wollen… ist das keine schlechte Idee. Ich bin ja nicht gegen TDD. Nur schmeckt mir der “Allmachtsanspruch” von TDD nicht. Im Rahmen eines durch Nachdenken entstandenen Lösungsansatzes kann TDD jedoch eine Hilfe bei der Implementierung dessen “Black Boxes” sein, d.h. der Operationen.

5. Ergebnissicherung

Das Ergebnis dieser Herangehensweise können Sie bei github einsehen. Tests und Produktionscode liegen in einem Gist. An dieser Stelle möchte ich daraus nur einen Teil zitieren, um zu zeigen, was ich unter Verständlichkeit verstehe:

Diesen Code kann man von oben nach unten lesen. Das Abstraktionsniveau sinkt von Methode zu Methode. Jede steht für einen Aspekt. Die Ebenen der obigen Zeichnung sind erhalten; der Code spiegelt damit den Entwurf. Oder umgekehrt: der Entwurf kann aus dem Code herausgelesen werden.

Wie funktioniert Word Wrap?

  1. In Wrap() hineinschauen und erkennen, dass dort einfach nur an eine Methode delegiert wird. Was sagt uns das? Hier ist Rekursion im Spiel. Das ist ein Pattern. Wrappen ist also eine Tätigkeit, die irgendwie mehrfach ausgeführt wird, um das Gesamtergebnis herzustellen.
  2. In Wrap_remaining_text() hineinschauen und dort die Schrittfolge sehen:
    1. Zeile extrahieren
    2. Zweile an neuen Text anhängen
    3. Das Ganze wiederholen mit dem restlichen Text [6]
  3. Und wie funktioniert das mit dem Extrahieren der Zeile? Einfach in der nächsten Methode nachschauen:
    1. Zeile einfach abschneiden vom Text
    2. Heilen eines eventuell dabei “verwundeten” Wortes

Und so weiter… Je nach Interessenlage des Lesers kann die Erkundung tiefer und tiefer vordringen. Jede einzelne Funktion ist überschaubar: der Name sagt den Zweck an, die wenigen Zeilen sind leicht zu entziffern.

Das ist grundsätzlich anders bei den im vorherigen Artikel zitierten Lösungen. Die sind monolithisch. Word Wrap findet dort irgendwie statt. Erfolgreich – jedoch wenig verständlich.

Reflexion

Ob ich “die beste” Lösung gefunden habe? Keine Ahnung. Ich habe eine ausreichend funktionierende gefunden; sie ist good enough. Und ist finde sie verständlich. Nicht nur, weil ich sie entworfen und implementiert habe, sondern weil die “Gedankenstrukturen” im Code sichtbar geblieben sind. Mehr ist mit heutigen Sprachen nicht zu wünschen, glaube ich.

Aber – so mögen Sie nun einwänden – die Lösung hat so viele LOC! Sie ist viel länger als Uncle Bobs.

Ja? Und? Warum ist das ein Problem? Macht das einen Unterschied bei der Compilation oder in der Laufzeit? Nein. Müssen wir auf Hauptspeicher oder Festplattenplatz achten? Nein.

Hat die Codierung deshalb länger gedauert? Das bezweifle ich. Aber selbst wenn, wäre mir das egal. Denn es kann nicht das Ziel der Softwareentwicklung sein, möglichst schnell Code zu schreiben, der funktioniert. Ziel muss es sein, Code zu produzieren, der lesbar und evolvierbar ist – ohne natürlich die funktionalen und nicht-funktionalen Anforderungen zu vernachlässigen.

Code wird viel öfter gelesen, als geschrieben. Deshalb sollte er fürs Lesen und nicht fürs Schreiben optimiert sein. Das habe ich getan, würde ich sagen. Das war zumindest meine Absicht.

Bonus: Nagelprobe Evolution

Ob Code clean genug ist oder nicht, kann man eigentlich nicht 100% entscheiden, indem man ihn nur liest. Seine Sauberkeit erweist sich erst, wenn man ihn verändern will. Wie geschmeidig ist er dann?

Das ist auch das Problem von 99% der Literatur. Dort Code präsentiert in eingefrorenem Zustand. Wie lange jemand dafür gebraucht hat, sieht man nicht. Wie gut die Struktur im Lichte der so wichtigen Evolvierbarkeit ist, sieht man nicht.

Also versuche ich es ein bisschen besser zu machen. Hier drei neue Anforderungen an den Code:

  1. Der umzubrechende Text besteht nicht aus einer, sondern aus mehreren Zeilen. Die können als Absätze verstanden werden.
  2. Zusammengesetzte Worte können bei Bindestrichen getrennt werden.
  3. Zeilen sollen im Blocksatz formatiert werden.

Wie genau diese Funktionalität aussieht, ist zunächst nicht wichtig. Sie zu implementieren, ist eine Fingerübung. Im Sinne der Evolvierbarkeit ist interessanter, wie leicht kann ich feststellen, wo überhaupt Veränderungen angebracht werden müssen? Werden bestehende Aspekte beeinflusst; welche? Sind neue Aspekte einzufügen; wo?

Für das alles, will ich natürlich nicht Berge an Code durchsehen müssen. Ich will auch höherer Abstraktionsebene erstmal darüber grübeln.

Blocksatz

Also: Wo müsste ich beim Blocksatz ansetzen? Muss eine Funktionseinheit, ich meine eine Methode, verändert werden? Sollte ich den Blocksatz irgendwo dazusetzen?

Blocksatz ist sicherlich ein ganz eigener Aspekt. Der hat nichts mit Texttrennung zu tun und auch nicht mit dem Zusammenbau des umgebrochenen Textes. Also darf keine bisherige Operation dafür verändert werden. Stattdessen sollte der Blocksatz zwischen den bisherigen Aspekten Textzerlegung und Textzusammenbau stattfinden. Weder muss die Textzerlegung davon wissen, noch der Textzusammenbau.

Der Blocksatz muss sich nur auf die gerade “abgebrochene” und “geheilte” Zeile konzentrieren. Das kann so im Code aussehen:

Wieder lasse ich bewusst Details weg. Weil ich es kann. Weil es überhaupt mehrere Abstraktionsebenen gibt in meinem Code. Wer sich für die genaue Implementation von Justify() interessiert, kann ja weiter “reinzoomen” im Gist.

Trennung bei Bindestrich

Wie ist es mit den Bindestrichen für die Trennung? Ist das ein neuer Aspekt wie der Blocksatz oder verändert diese Anforderung einen bestehenden?

Ich würde sagen, hier geht es um eine Variation der Operation “Schnitt korrigieren” im ersten Entwurfsbild bzw. Schritt 2 in der verbalen Beschreibung des Lösungsansatzes.

Falls ein Wort zerschnitten wurde, wird sein “Kopf” nicht einfach komplett wieder an den “Rumpf” im Resttext geheftet, sondern es wird nach einem Bindestrich im “Kopf” gesucht und nur der dahinter liegende “Teilkopf” wird angeheftet.

Hier dazu die neuen Testfälle für die “Heilung” der Zeilenabspaltung:

Dadurch ändert sich natürlich einiges am Arbeitspferd der “Heilung”. Dieser Aspekt bekommt Sub-Aspekte – eine Zeile kann entweder mit einem zerschnittenen Wort enden oder mit einer “Silbe”:

…die sich dann auch im Code niederschlagen müssen:

Und nochmal: Wer mehr Details sehen will, “zoomt rein”. Für ein Verständnis des Lösungsansatzes sind das nicht nötig.

Mehrzeilige Texte

Für die Behandlung mehrzeiliger Texte ist zunächst zu klären, wie der API aussehen soll. Liegen die Texte als ein string vor, in dem Zeilen durch \n abgetrennt sind? Oder liegen mehrere Texte z.B. in Form eines IEnumerable<string> vor?

Ich nehme mal an, dass es weiterhin nur ein string ist, in dem jetzt auch schon Zeilenumbrüche vorhanden sein können. Damit ändert sich der API nicht.

Diese Zeilenumbrüche sollen natürlich erhalten bleiben. Das ist in einem Testfall zu spezifizieren:

Der Lösungsansatz sieht dafür dann im Grunde aus wie der bisherige:

  1. Etwas von einem Rest abtrennen, nämlich den nächsten Text
  2. Das Abgetrennte verarbeiten, nämlich den bisherigen Word Wrap darauf ausführen
  3. Das Verarbeitungsergebnis an das bisherige Resultat anhängen

Graphisch wächst das Modell sozusagen nach oben. Es gibt eine neue Aspekt-Ebene oberhalb des bisherigen Word Wrap: das Word Wrap (multi) :-) Es zerfällt in das Word Wrap in drei Sub-Aspekte – von denen der letzte derselbe wie beim Word Wrap ist, der Textzusammenbau.

Bei soviel Ähnlichkeit liegt es nahe, die Struktur der Implementation zu kopieren. Eine Rekursion ist auch hier gleichermaßen elegant wie verständnisförderlich.

Wie funktioniert der Code? Wrap_multi_text() verrät es. Einfach wieder von oben nach unten lesen. Der Lösungsansatz steht dort 1:1 übersetzt.

Fazit

Tut mir leid, ich kann nicht anders als zu sagen: Das war alles ganz einfach mit ein bisschen nachdenken. Think a little, code a little.

Ich hoffe, Sie sehen das genauso: Der Code ist verständlich und evolvierbar. Dass er nicht der kürzestmögliche ist, sei zugestanden. Aber ist das wirklich sooo wichtig? Nein. An LOC zu sparen, stolz auf eine kurze-knappe Lösung zu sein, halte ich für eine premature optimization.

Nichts ist natürlich gegen Eleganz zu sagen, wenn Sie die Verständlichkeit und Evolvierbarkeit erhöht. Rekursion statt Schleife finde ich deshalb für diese Kata auch so gut wie Robert C. Martin. Aber an LOC sparen zu wollen, um vielleicht hier und da ein bisschen schneller mit dem Schreiben fertig zu werden, scheint mir wenig nachhaltig. Das ist nicht in die Zukunft gedacht, in der der Code noch viele Male gelesen und verstanden werden muss.

Zum Schluss auf den Punkt gebracht. Was habe ich getan? Ich habe ganz konsequent…

  • das SRP angewandt,
  • das Prinzip SLA angewandt,
  • möglichst die grundlegenden Semantikaspekte Integration und Operation getrennt,
  • Domänenaspekte für sich getestet.

Versuchen Sie das bei der nächsten Code Kata doch auch einmal. Aspektvolles Programmieren hilft.

Fußnoten

[1] Es täte mir leid, wenn ich damit zu pauschal urteilen würde. Wenn irgendwo die Dojo-Praxis davon deutlich abweicht, dann melde man sich. Ich werde mich dann bemühen, persönlich mal dabei zu sein und zu lernen, wie es auch anders gehen kann.

[2] Dass es inzwischen hier und da Coding Dojos oder Code Retreats gibt, die sich mit Legacy Code befassen, ist löblich. Die Frage ist nur, inwiefern dort konzeptionell an die Sache herangegangen wird. In welchem Rahmen wird dort über Pathogenese und auch Salutogenese von Software nachgedacht?

[3] Ob ein Wortumbruch statt eines Zeilenumbruchs sinnvoller wäre, lässt sich nicht entscheiden. Es gibt keinen Kontext für die Code Kata. Die Aufgabe fordert ihn jedenfalls nicht. Und wenn wir im Sinne von YAGNI auch lernen wollen, genau hinzusehen und nur zu implementieren, was wirklich gefordert ist, dann tun wir gut daran, uns an die Aufgabe zu halten – zumindest solange niemand da ist, mit dem wir über ihre Sinnhaftigkeit diskutieren können.

[4] Wer dabei auch mal etwas ausprobieren muss, soll das gern tun. Nachdenken darf durch Hilfsmittel jeder Art unterstützt werden. Bei den üblichen Code Katas sollte das jedoch eher nicht nötig sein. Die sind rein algorithmisch und sehr überschaubar. Ausprobieren im Sinne von Codieren sollte nicht nötig sein.

[5] Damit will ich nicht sagen, dass man die Lösungsentwicklung und Codierung personell trennen sollte. Auf keinen Fall! Aber wir sollten anerkennen, dass das eben zwei unterschiedliche Tätigkeiten sind, die auch unterschiedliche Kompetenzen brauchen. Deshalb sollten sie nicht pauschal vermischt werden, wie es bei TDD in der Literatur der Fall ist.

[6] Bitte beachten Sie den Gebrauch von dynamic hier. Dadurch wird der Code leserlich, ohne dass ich für den Zweck der Zusammenfassung einer Zeile mit dem verbleibenden Rest einen eigenen Typen einführen muss oder auf Tuple<> ausweiche.