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:
- Vom gegebenen Text bricht man eine Zeile von Maximallänge vorne ab. Es entstehen eine Zeile und ein Resttext.
- 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.
- Die Zeile kann jetzt an den bisher schon erzeugten umgebrochenen Text angefügt werden.
- 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?
- 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.
- In Wrap_remaining_text() hineinschauen und dort die Schrittfolge sehen:
- Zeile extrahieren
- Zweile an neuen Text anhängen
- Das Ganze wiederholen mit dem restlichen Text [6]
- Und wie funktioniert das mit dem Extrahieren der Zeile? Einfach in der nächsten Methode nachschauen:
- Zeile einfach abschneiden vom Text
- 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:
- Der umzubrechende Text besteht nicht aus einer, sondern aus mehreren Zeilen. Die können als Absätze verstanden werden.
- Zusammengesetzte Worte können bei Bindestrichen getrennt werden.
- 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:
- Etwas von einem Rest abtrennen, nämlich den nächsten Text
- Das Abgetrennte verarbeiten, nämlich den bisherigen Word Wrap darauf ausführen
- 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.
5 Kommentare:
Puh, Ralf. Das ist mal starker Lesestoff. Ich schicke mir die Seite auf den Kindle und les' sie heute Abend. :)
Bis dahin kann ich Dir aber schon mal Recht geben: Verständlicher Code ist mehr als nur Code + (vernünftiger) Kommentar.
Gerade erst intern beim "Herumspielen" mit TDD gehabt. Wir haben überlegt, wie sich TicTacToe testgetrieben entwickeln ließe. Sprich: Fokus erst einmal weg von der UI hin zum Algorithmus.
Mein Ergebnis ließ sich in drei Teile brechen:
1. Wie speichere ich das Spielfeld?
2. Wie merke ich mir ein gesetztes Feld?
3. Wie finde ich heraus, ob eine Gewinnerkombination dabei ist?
Kurzum: Ich habe den Weg gewählt, das Spielfeld an eine Player-Klasse zu hängen und damit jedem Spieler seine eigene Variable zu gönnen, in der seine eigenen Häkchen gespeichert werden. Damit hat es sich reduziert auf zwei Möglichkeiten: Feld gesetzt. Feld nicht gesetzt. Oder: 1 und 0. Und darauf baute meine Lösung auf.
1. Wie speichere ich das Spielfeld?
Lösung:
int spielfeld;
2. Wie merke ich mir ein gesetztes Feld?
Lösung:
spielfeld |= 1 << pos;
3. Wie finde ich heraus, ob eine Gewinnerkombination dabei ist?
Lösung:
var gewinnerKombinationen = new List { 448, 56, 7, 292, 146, 73, 27, 84 };
...
return gewinnerKombinationen.Exists(x => ((spielfeld & x) == x));
Und hier geht's los... zwar mathematisch korrekt und echt mit Stil programmiert... versteht keine Sau, was das soll.
448? 56? 27? Was sind das für Zahlen? Klar... meine Idee war im Prinzip simpel:
_ _ _
|8|7|6|
|5|4|3|
|2|1|0|
Angenommen, das sei das Spielfeld und ich würde es derart durchnummerieren, dann könnte ich gesetzte Felder darauf als eine Aneinanderreihung von Bits verstehen.
Beispiel:
_ _ _
|x|x|x|
|_|_|_|
|_|_|_|
= 111000000 = 448
Und das Setzen von Feldern geht einfach durch eine Bitverschiebung der Zahl 1 und eine Oder-Verknüpfung mit dem Feld, sowie das Ermitteln von Gewinnern einer bitweisen Und-Verknüpfung aller Kombinationen jeweils mit dem Spielfeld entspricht.
Aber den Code? Den verstehen keiner (auf Anhieb).
Spannend fand ich daher einen Hinweis aus der Community:
Wozu int? Oder Int32? Oder UInt32? Oder Short... oder was auch immer. Du brauchst Bits. Und zwar neun an der Zahl. Was Du brauchst ist ein BitArray.
Ich:
Toll... wie sieht das denn aus im Code?
new BitArray(new bool[]{ true, true, true, false, false, false,...})
Die Lösung:
const bool x = true;
const bool o = false;
new bool[]{
x,x,x,
o,o,o,
o,o,o })
Etc. PP.
Klar ist das mehr Code... aber hier wird sofort verständlich, was er bedeutet.
Das Setzen ist sogar richtig einfach zu verstehen, da BitArray eine Set Methode mit denkbar einfacher Signatur anbietet:
spielfeld.Set(pos, true);
Einzig das Ermitteln des Gewinners erfordert eine Hilfsmethode, die aber ebenfalls einfach zu verstehen ist. Die eigentliche Abfrage lässt sich Dank Lambda dann aber dennoch einfach formulieren:
gewinnerKombinationen.Exists( x => Matches(x, spielfeld))
Fazit
Was uns als Entwickler häufig schwerzufallen scheint, ist mehr über den Code nachzudenken, den wir schreiben, einen Schritt zurückzutreten und dabei zu überlegen, wie andere ihn sehen. Aus meiner Sicht ist die Zeit monolithischer One-Man-Shows lange vorbei. Wir können es uns nicht leisten, dass andere unseren Code nicht verstehen. Und schon gar nicht, dass wir ihn selbst nach entsprechender Zeit nicht mehr verstehen.
Und dabei würde es einfach nur reichen, wenn wir mehr darüber nachdenken würden, was wir tun... und dabei Fragen stellen wie: Wird daraus offensichtlich, was hier passiert, oder muss ich für das Verständnis dazu an anderer Stelle Informationen einholen?
@Christian: Freut mich, dass der Artikel für dich interessant aussieht.
Dass ihr TTT mal testgetrieben entwickeln wolltet... schöne Idee - wenn auch unrealistisch ;-) Denn gerade mit TTT wäre es schön zu sehen, wie sich nämlich eine Lösung anders entwickelt, wenn man mal wirklich agil und nicht durch die OOTDD Brille schaut.
Woher du zum Beispiel eine Player-Klasse hast... keine Ahnung ;-) Ich komme nie auf die. Und ich wäre auch nicht auf ein Bitarray gekommen. Denn wie stellst du darin denn ein nicht belegtes Feld dar? Du brauchst ja Tri-State: nicht belegt, X O.
Aber hier ist nicht der Ort, um über TTT zu sprechen. Bin gespannt auf Kommentare zu Word Wrap.
Ach, noch eins: Nur weil Entwickler heute zu mehreren an einer Anwendung sitzen, bedeutet das nicht das Ende von Monolithismus. Es kommt nämlich darauf an, wi diese Gruppe die Software angeht. Der Monolith beginnt im Kopf oder gar in der Kultur des Unternehmens. Daher verschwindet er nicht, wenn mehrere Köpfe irgendwie auf IDEs schauen.
Nette, gut nachvollziehbare Lösung. So weit, so gut.
Ich wäre vermutlich niemals auf die Idee gekommen, WordWrap als Rekursion zu implementieren. Irgendwie habe ich was gegen dieses Pattern, vermutlich weil es eine endliche Menge an Informationen in etwas per Definition unendliches verwandelt.
Ich bevorzuge den iterativen Weg, und ich will das auch begründen. Für mich sind die grundsätzlichen Aspekte Textzerlegung und Textzusammensetzung bereits gelöst.
Ich habe bereits eine allgemeingültige Zerlegungsfunktion, eine String.Split-Funktion, die Text per Lambda inein IEnumerable splittet. Für die Textsuammensetzung gibt es eine entsprechende Join-Variante. Alles trivial, tausendfach eingesetzt, testgesichert -> ich will den Mist nicht nochmal testen.
Damit kann ich mich auf den explizit nicht genannten Aspekt 'wie zerlege ich Einen Text wort- oder gar silbenbasiert' konzentrieren. Zum einen, weil dies die Grundvoraussetzung für mannigfaltige Applikationen schafft, zum anderen sind hier die meisten Problem zu erwarten. Das ist im Falle Wörter recht einfach lösbar, im Falle Silben hätte ich instinktiv StackOverflow bemüht, ob jemand dieses Problem schon mal hatte. Dann wäre ich mehr oder minder sofort zu einer Teilimplementierung der Tex-Funktionalität gelangt. So oder so: dieser Bestandteil der Implementierung bleibt stabil, Erweiterungen im Bereich der Mehrsprachigkeit sind ohne Probleme nachrüstbar. Also kann ich hier den ersten Komposit zweiter Ordnung identifizieren und per Test sichern.
Die Frage bei der Textzusammensetzung (also dem eigentlichen Umbruch) lautet dann: kann ich die nächste Silbe noch an die aktuelle Zeile anhängen, ggf. mit Trennstrich wenn darauf eine weitere Silbe folgt?
In der Kata ist das einfach per Buchstabenzählung vorgegeben. In der Lösung wird diese Zählung dann annähernd durch den gesamten Aufrufbaum geschleust.
Realistisch ist aber die Angabe einer Breite in Pixel und einer Schriftart (auch wenn die Kata davon nicht spricht, aber wir wollen doch Evolution erlauben).
Deshalb evolviert für meine Begriffe die Lösung auf einer zu niedrigen Ebene. Ich erlaube Evolution nur im Komposit.
Das ist im vorliegenden Fall die Wrap-Methode. Eine zweite Signatur mit [widthPx] und [font], die dann ein neues Zusammensetzungslambda einsetzt (und hier bitte keine Angst vor Copy-Paste) täte gut.
Das Lambda selbst ist innerhalb von Minuten ohne irgendwelche Probleme anpassbar. Ich muss keine weitere Funktion anpassen.
Und wenn RTF-Annotationen hinzukommen, dann wird es im Prinzip nicht schwieriger. Natürlich muss ein anderer Parser gewählt werden (also eine andere Zerlegungslogik), es entfällt der [font]-Parameter und es ändert sich die Logik zur Bestimmung der Silbenlänge.
(Tatsächlich habe ich eine Implementierung genau so laufen, Zweck dort ist das Drucken von RTF-Text.)
Ist das überhaupt test-driven oder agil? Keine Ahnung, politische Begriffe interessieren mich auch nicht besonders. Das betrifft auch die Definition der Problemdomäne. Für die FD-vorgehensweise interessieren mich Input, Verarbeitungslogik und Output.
Hallo Ralf
Ich finde deine 2 ersten Schritte
"1. Domäne spezifizieren" und "2. Lösung skizzieren" auf jeden Fall sinnvoll, würde aber sagen dass TDD diese nicht ausschliesst!
>> 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.
Ist es nicht so, dass erst wenn man Tests schreibt man erkennen kann wie gut oder schlecht der Code ist??
Das ist ein starker Zusammenhang zwischen Testability und gutem Design.
Gut zusammengefasst durch Michael Feathers in: The Deep Synergy Between Testability and Good Design
http://vimeo.com/15007792
>> Damit sind wir beim blinden Fleck der TDD-Coding-Dojo-Praxis.
>> Über dieses “issue behind programming” wird nicht nachgedacht.
Ja. Weil einfach los codiert wird? Aber die eigentliche TDD Praktik sagt. Refactor!! -> Think!!
Ich glaube wie man TDD praktiziert macht hier einen grossen Unterschied.
>> 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]
Siehe Kommentar vorher. Hängt ab wie man die Übung macht. Jede Übung kann man gut oder schlecht üben.
Genau um das geht es in Marc Gladwell’s Buch: „Outliers“
Was Erfolgreiche gemeinsam haben: 10000 Stunden üben.
Aber nicht einfach Blindes Üben! Sondern „Deliberate practice“. D.h. Gezieltes Üben: Mentor, Challenge, Feedback, Dedication…
Ist deine Hauptaussage, dass TDD meistens falsch geübt wird?
Da stimme ich dir zu ;-)
@Peter: Ja, ich glaube, dass TDD meist falsch geübt wird. Zu wenig deliberate.
Aber darüber hinaus glaube ich auch, dass TDD eine Lücke hat. Und die liegt im "schwammigen" Refactoring. Kann man machen - muss man aber nicht ;-)
Doch, ja, ich weiß, muss man - nur da fängt es dann an, schwierig zu werden. Man lässt es also schnell mal sein. Weil man grad nicht so recht weiß, wo ansetzen, und der nächste Testfall ja auch noch schnell umgesetzt werden will.
Refactoring für sich ist eine ganz eigene Disziplin. Und wenn dann Leute mit TDD anfangen, die keinen ausgebildeten Sinn für Refactoring haben, dann kommt bei TDD wenig raus.
TDD hat einen hohen Anspruch, der auf dem Refactoring ruht. Aber TDD tut nichts fürs Refactoring, sondern überlässt alles dem Entwickler. Da stimmt was nicht.
Deutlich besser ist da "TDD as if you meant it". Da gibt es keinen Ausweg. Man muss Refactoring betreiben - und schon sehen die Ergebnisse deutlich besser aus.
Kommentar veröffentlichen