Sonntag, 4. November 2012

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.

Kommentare:

sm0n hat gesagt…

Ich denke, dass viel um eine Lösung herumprogrammiert wird, anstatt sich um das Problem Gedanken zu machen.

Die Unterteilung in die Teilschritte ist da schon ein Schritt in die richtige Richtung, erst einmal das Problem zu verstehen und auch danach die Tests zu schreiben.

Der normale Weg ist eher so:

* eine Anforderung taucht auf
* die Lösung wird quasi mitgeliefert ("Das müsste rauskommen...")
* es wird ein Test auf Ausgabe geschrieben (nicht auf Funktionalität)
* das Ganze wird implementiert und gut ist

So weit wäre die Welt ja in Ordnung, wenn nicht

* die Anforderung nicht ganz korrekt umgesetzt wurde ("Bei dem einen geht's, aber...")
* ein neuer Test notwendig ist
* und die Implementierung angepasst wird ("Na dann schreiben wir das eben ein bisschen um..." Q&D)

Meine Kritikpunkte

* der Code eine Art One-Hit-Wonder ist ("Es ist doch grün...?!")
* dass nur Workarounds geschaffen werden, anstatt das neue Wissen in den Code einfliessen zu lassen
* TDD, Refactoring usw. werden als Allheilmittel verstanden ("M. Fowler hat das in seinem Buch aber anders geschrieben...") anstatt als Werkzeuge

Gerade das Verständnis im Einsatz von Werkzeugen (und das sie es sind) kommt m.E. auf Konferenzen, Treffen, Dojos, Vorlesungen usw etwas zu kurz... leider.

Man möge mich eines besseren belehren (der Leser unsere Code-Zeilen wird es danken :)

Sageniuz hat gesagt…

Danke vielmals für diesen wirklich ausgezeichneten Post. Ich stimme mit dem Geschriebenen vollkommen überein.

Peter Kofler hat gesagt…

Ja das stimmt, dass TDD alleine uns nicht vor schlechtem Code schützt, und schon gar nicht vor schlechten Tests. Ich sehe das immer öfter dass der Fokus auf Konferenzen von "Use TDD" auf "Manage Your Testing Hell" wandert.

Danke für den Hinweis zu Word Wrap, ich werde dementsprechend einen neuen Post verfassen, mit Fokus auf Tell-Don't-Ask und Domänenkonzepten. Vielleicht kann dieser dann ja als positives Beispiel dienen ;-)

Anonym hat gesagt…

Ich kann dem auch nur zustimmen, bin aber mit dem Beispiel und der Lösung nur bedingt zufrieden.

Nehmen wir jetzt die neue Anforderung: Blockssatz. D.h. die Umbrüche sollen so gemacht werden, dass man das Textbild "schön" zum einstellten Font aussieht. (Man muss also die Pixel der einzelnen Zeichen als Umbruchkriterien mit hineinnehmen).
Eine weitere Anforderung wäre das Polygonzüge (einfachster Fall: Rechteck), die Umbruchgrenze markieren.

Grundsätzlich sollte die (jede) Aufgabe sinnvoll zerlegt werden und der Code soll dieses direkt erzählen. Aber die Antizipation oder Extrapolation auf die nahe Zukunft steht im Konflikt zu KISS und YAGNI (die Betonung SILBEN halte ich für überbewertet).

Zuoft sieht man schlecht strukturierten, undurchsichtigen Code, unpassendes Abstraktionsniveau oder einfach Verletzung von separations-of-concerns und man fragt sich: was mag der Code wohl tun? welche Gedanken hatte der Autor?

Leider findet man auch schlechten Code in Zeitschriften, Blogs und auch in produktiven Systemen :-(

Ralf Westphal - One Man Think Tank hat gesagt…

Freut mich, dass die Kommentare bisher so positiv waren. Hätte ich nicht gedacht. Dann hab ich wohl etwas angesprochen, was auch andere schon auf der Seele lag. Fein.

@Anonym: Mit welchem Beispiel bist du nicht zufrieden? Kata Wordwrap finde ich genauso gut wie Kata Bowling oder sonst eine. Die Aufgabe kann ja nicht klein genug sein, um nicht schon Probleme zu zeigen. Denn wenn die Unverständlichkeit schon bei so trivialem Zeug auftritt, dann wird das bei Größerem nicht besser.

Mit welcher Lösung bist du nicht zufrieden? Ich habe doch keine eigene angegeben. Die kommt erst noch ;-)

Den Blocksatz als neue Anforderungn finde ich auch gut. Vielleicht nehme ich die sowie die mehreren Absätze und mehrzeiligen Quelltexte mal als weitere Iterationen in meine Lösung auf. Einfach um beispielhaft zu zeigen, was sich dadurch ändert.

Zu den Silben sage ich noch etwas, wenn ich meine Lösung vorstelle. Nur soviel hier: davon bin ich auch erstmal wieder abgerückt.

McZ hat gesagt…

Die Frage ist doch, ob die Vorgabe der Testmethodik nicht bereits die Kodierung vorgibt. Bzw, ob sich die vorgegebenen Testfälle wirklich mit testwürdigen Aspekten befassen, oder ob sie zu hoch ansetzen und zu 80% immer die gleichen Trivialitäten testen. Wobei man sich dann wundert, dass Testfälle in die Dutzende ausarten.

Vielleicht steckt da auch ein Missverständnis dahinter. TDD ist für mich kein architektonisches Mittel. TDD ist auch kein Tool, um "Fehlerfreiheit" zu garantieren. Es gibt keinen mathematischen Korrektheitsbeweis; vielmehr handelt es sich um so etwas wie Pseudo-Induktion ("für die definierte Menge von Eingabewerten sind die berechneten Ergebniswerte korrekt; also sind vermutlich auch alle anderen Fälle korrekt; genau wissen wir das aber nicht"). Unterm Strich sind Tests ein statistisches Mittel, um ein Paradox zu umschiffen: die Funktion ist quasi ihr eigener Beweis.

In dieser Hinsicht bekommt man das, was man via Testcase bestellt. Und so kommt dann halt Mach-mich-grün-Code zustande.

Grundsätzlich denke ich, dass wir Kodierung viel zu kompliziert machen. Nehmen wir WordWrap; ich würde es in folgende Teilschritte zerlegen:

1) Text in Bereiche (also Worte oder Silben, zzgl. Whitespaces usw) zerlegen
2) Bereiche gemäß maximaler Breite und dem Vorhandensein eines Zeilenumbruchs trennen.
3) Jede abgetrennte Sequenz zu einer Zeile zusammenführen
4) Alle Zeilen durch Zeilenschaltung zu einem Ausgabetext zusammenführen

Darauf folgt Pseudocode. In meinem Falle - FD-basiert und LINQ-like mit Erweiterungsmethoden - komme ich direkt auf:

strBody _
.Split(x, x => ...) _
.Split(x, y => ...) _
.Select(x => String.Join(x)) _
.Join(crlf)

Und da Lambdas nicht gut zu testen sind wird daraus:

strBody _
.Syllables(de-DE) _
.Wrap(200) _
.Select(x => String.Join(x)) _
.Join(crlf)

Syllables und Wrap sind hierbei schlicht Komposite mit Inhalt Split(x, y => ...), wobei in dem Lambdas der Fähigkeit, eine Sequenz in Subsequenzen zu trennen, lediglich die der Problemstellung adäquate Trennungslogik beigefügt wird.

Diese additive Logik kann man hervorranged mit TDD entwickeln. Man braucht Split nicht mehr testen, weil der Test bereits existiert. Man testet nur noch hinzukommmende Funktionalität, statt sich mit bekannten und gelösten Fragestellungen und als solches Trivialitäten zu befassen.

Man dreht damit den Spies um. So lange die einzelnen Teile getestet sind und Ihren Vorgaben entsprechen, so lange funktioniert auch das Komposit. Das Komposit selbt muss nicht getestet werden.

Carsten hat gesagt…

hier mal meine 50cts:
TDD kann ein durchaus "Design"-Tool sein - aber IMHO eben nicht für Algorithmen sondern eher für die Schnittstellen zum Code.

Wenn eine extrem einfache Funktion in sehr viele Testfälle ausartet, dann ist das für mich ein schönes Zeichen, dass man konkrete Werte testet, während man entweder nur Randfälle und einen typischen Fall oder eben gleich Eigenschaften testen sollte!

Beispiel: eine Funktion, die eine Kollektion sortiert, muss eine Kollektion liefern, deren Elemente eben aufsteigend oder absteigend sortiert sind - das kann man sehr einfach durch einen Durchlauf und paarweise Vergleiche testen.
Frameworks die so etwas mit zufälligen Daten machen gibt es ja mittlerweile auch in .net (FsCheck - Vetter von QuickCheck).

TDD ist super - ich muss nicht bei jeder Änderung/Refaktorierung angst haben, dass ich anderen Code zerschossen habe (zumindestens kann ich mir *sicherer* sein, also nur mit einem groben Funktionstest).

Aber ob jemand mit TDD auf so etwas wie z.B. QuickSort gekommen wäre? Ich weiß nicht - ich glaube da bleibt man bei dem hängen was man naive machen würde - sprich sehr wahrscheinlich einer sehr langsamen Variante (wie z.B. "(findFirst col)++(remove (findFirst col) col)" wenn ihr wisst was ich meine)

Anonym hat gesagt…

Hallo Ralf,

"Kata Wordwrap" ist okay, hingegen ist die Kritik an den vorgestellten Lösungen nur bedingt berechtigt, denn aufgrund des Kata-Charakters gehe ich von einem geringen Reifegrad aus => d.h. nur die Tests sind grün. Mit dieser Vorstellung sind alle Lösungen okay, weil auch sie in der Tat recht klein/umfangreich und vermutl. auch keine hohe Reife besitzen.

Mit Blocksatz wollte ich ein Beispiel liefern, das auch dein ersten Entwurf für die Problemzerlegung nicht gerecht wird, wo du ja ausdrücklich auf SILBEN hingewiesen hattest.

SIMPL, KISS, YAGNI, Antizipation und sprechender Code sind eine ausreichend gute Basis, qualiativ hochwertigester Code entsteht nach und nach und nicht sofort in Iteration 1.

Thomas hat gesagt…

Hallo Zusammen,
McZ deine Einleitung finde ich hervorragend. Ich würde sogar soweit gehen zu behaupten
das "TDD" ein extremer Ansatz ist. Die Konzentration auf einen Aspekt der Entwicklung (Hier Testen)
macht meiner Meinung nach den Entwicklungsprozess unausgewogen.
Der zweite Punkt den ich bei TDD bemängele ist die Qualität der Entwicklertests. Jeder der schon mal
in der entwicklungsbelgeitenden Qualitätssicherung gearbeite hat oder sich mit den Methoden beschäftigt
hat ( z.B. ISTQB), weis das man sein eigenes Produkt / Entwicklung 'schlecht' testet. Das ist nichts schlimmes
das liegt an uns Menschen.
Ich habe die Erfahrung selber gemacht als ich Unittests für meine Code geschrieben habe. Ich war
kritischer beim Test mit den Entwicklung anderer, als mit meinen eigenen. Ich denke es liegt daran,
dass man seinen eigene Code, sein eigenes Produkt, seine eigene Entwicklung als sein 'Baby' ansieht und man
selber stolz drauf ist. Natürlich will man seinem 'Baby' nichts böses tun und nicht kritisch
(durch tests) gegenüber sein. Man beginnt halt Tests zu entwickeln die der Code besteht. Man kennt
auch die Schwachstellen des eigenen Codes, aber man sagt sich natürlich 'der Code wird nie so eingesetzt
- warum einen Test entwicklen?'. Und aus Projektmanagementsicht würde ich dazu sagen: "Warum soll
ich Geld in die Entwicklung von Testfälle stecken, die eine geringere Qualität haben, als Testfälle
die von Aussenstehenden (also nicht von den Softwareentwicklern) geschrieben sind."

sm0n hat gesagt…

@McZ: Ich stimme dir ebenfalls zu, aber ich denke, dass das Problem tiefer liegt...

"Pseudo-Induktion" trifft's denke ich schon, eben eine TDD-Gläubigkeit ("Ist ja grün, dann kann's ja nicht falsch sein..."). Ist in diesem Fall aber eher ein falsche Annahme/Vorgehensweise.

BTW: Die Trennung würde ich auch so machen :)

Robert hat gesagt…

Ein sehr treffender und guter Post! Ich hab mir bei vielen Dojos schon das gleiche gedacht (es ist zwar gelöst, aber kein Clean-Code).

Die Preisfrage ist: Wie kann man bei den Teilnehmern ein Bewusstsein dafür schaffen, dass sie Clean-Code lernen sollen? Sie sind leider auch ohne glücklich...

Anonym hat gesagt…

Die grosse Zustimmung zum Clean-Code möchte ich gern aufgreifen und mehr als das verlangen:

@Robert, alle:
Wie sieht denn jetzt eine Ideallösung aus?

Müssen wir auf Ralf warten?

Reicht im ersten Schritt nicht auch eine 70% Lösung?
Haben die Deutschen ein Problem mit Over-Engineering?
Was darf die Lösung dieser Trivialaufgabe kosten?

Ralf Westphal - One Man Think Tank hat gesagt…

@Anonym: Auf mich warten? Wieso? Wer? Ne, auf mich muss keiner warten. You can clean code now! :-)

Overengineering? Ja, davor haben viele Angst. Mit der Nationalität hat das eher nichts zu tun, würde ich sagen. Das hat was mit dem mindset unserer Zunft zu tun. Und da gilt: Eleganz! Die hat mit Sparsamkeit der Mittel zu tun.

Und dann kommt KISS! Bei YAGNI zuckt niemand. Da wird munter alles mögliche drauflosprogrammiert - nur KISS muss es sein.

So grassiert eine Aversion gegen "Overengineering". Was oberflächlich natürlich auch richtig ist. In Overengineering steckt ja schon ein negatives Urteil. (Dass Leute, die ohnehin zuviel tun, das aber nicht bemerken (wollen), die Overengineering-Watschen verteilen, ist natürlich eine Ironie.)

Overengineering wie auch jeder andere Qualitätsmangel entsteht ja aber ausschließlich in Relation zu Werten. Welche sind denn das aber? Und welche Werte berücksichtigt man nicht bei dem Urteil?

Wenn ein Wert lautet "Möglichst wenige LOC produzieren", na, dann mag meine Lösung für Wordwrap overengineert aussehen.

Aber wenn ich dagegen halte "Evolvierbarkeit" und "detailliertere Testbarkeit"... dann sieht es anders aus, finde ich.

Naja, nun muss ich mal demnächst meinen Lösungsvorschlag präsentieren, glaub ich ;-)

Christian Götz hat gesagt…

@Robert:
Die Preisfrage ist: Wie kann man bei den Teilnehmern ein Bewusstsein dafür schaffen, dass sie Clean-Code lernen sollen? Sie sind leider auch ohne glücklich...

Das könnte ich mir nur durch den geringen Umfang und der quasi nicht vorhandenen Komplexität der Katas erklären.

Für diese kleinen Spielereien braucht man nicht zwingend Clean-Code.

Hier sind vielleicht die Veranstalter selber gefragt, ihre Dojo-Sessions entsprechend zu gestalten.

Sie müssten davon weg kommen, immer wieder TDD beizubringen, sondern den Fokus mehr auf echte Software-Entwicklung richten.

Unit Tests können nützlich sein, sind aber nicht zwingend notwendig, um funktionierende und korrekte Software zu erstellen.
(Es stellt sich mir sowieso die Frage, wie man die Korrektheit der eigentlichen Tests gewährleisten will.)

Vielleicht sollte wieder mehr das Gefühl für aussagekräftigen Quellcode geschult werden. Also, dass es eben nicht cool ist, kryptischen Code eingetippt zu haben, den man nach einem Monat selber nicht mehr auf Anhieb versteht.

Irgendetwas in der Art. Aber bitte nicht mehr die immer gleichen TDD-Sessions, wo man vorher sein Gehirn auf Energiesparmodus schaltet und dann total naiv zu programmieren anfängt. (Also erst muss der Test rot werden und dann nur gerade so viel implementieren, dass der Test grün wird. Aber auf keinen Fall versuchen, weiter oder überhaupt erst zu denken. Das macht den ganzen TDD-Prozess kaputt. Immer schön dumm anstellen. Nur der Test leitet einen. ;-D)

Ralf Westphal - One Man Think Tank hat gesagt…

@Christian: Das hört sich doch gut an. Markig gesagt :-)

Deshalb bieten Stefan Lieser und ich ja schon länger Application Katas an: http://clean-code-advisors.com/ressourcen/application-katas

Das sind größere Katas, an denen man sich mal versuchen kann.

Und ein ganz einfacher weg, um zu mehr Clean Code zu kommen:

1. Die Aufgabe größer wählen, z.B. App Kata.
2. Die Aufgabe in feine Scheiben zerlegen, sagen wir mal 3-4, also Inkremente.
3. Alle Dojoteilnehmer (oder in Pairs) eine Scheibe implementieren lassen. Dann gibt es n Lösungen.
4. Alle "Teams" rücken einen Platz weiter, d.h. an einen anderen Rechner und eine andere Lösung. Dort implementieren sie das nächste Inkrement.
5. goto 3 - solange noch Inkremente zu fertigen sind.

Aus Erfahrung kann ich sagen: Beim Wechsel zu einer fremden Lösung fliegen viele Kraftausdrücke durch den Raum :-)))

Anonym hat gesagt…

@Ralf: das ist ein guter Vorschlag.

Bezogen auf die berufliche Programmiertätgkeit fallen mir, neben Pair-Programming, die allzuoft vernachlässigten Codereviews ein.

D.h. man setzt sich mit dem Code des Kollegen auseinander und diskutiert diesen im Rahmen des Reviews (ggfs. öffentlich, teamweit).

Meinen Eindrücken nach führt die Diskussion über Code insgesamt wieder für alle Beteiligten zu besserem Code (allein vielleicht dadurch, dass der Code wieder gereviewt werden kann).

Die dahinter liegende Frage ist vielmehr, was ist guter Code? Ein schwierige Antwort.
@alle: Was ist guter Code?

Einfacher ist hingegen schlechten Code aufzudecken. Die Meinungsvielfalt ist in Detailfragen gross, aber andererseits liefern Reviews ein grossen Konsens für guten Code (ein Kriterium ist Verständlichkeit).

"Tests are overrated" - http://www.infoq.com/presentations/francl-testing-overrated
verdeutlicht die Bedeutung des Werkzeugs Codereview als qualitätssteigernde Massnahme.

McZ hat gesagt…

"Was ist guter Code?"

Jeder Code, der es schafft, neben der Erreichung einer Zielstellung auch noch den Weg dorthin ohne Hilfsmittel verständlich zu machen.

Und da hätten wir schon diverse Probleme mit unserem Hang, state-of-the-art sein zu wollen.

Beispiel: ein Kollege von mir setzt für JEDE Lösung das Entity Framework ein. Das ist natürlich bequem, und es tut was es soll. Aber er bekommt dann und wann schon mal so seine Problemchen, z.B. wenn ein String-Feld in der Datenbank auf einen benutzerdefinierten Typ serialisiert werden soll. Oder wenn ein Feld beispielsweise Serialisierungsoptionen für ein anderes Feld enthält.

Und das Ganze nur, weil er sich weigert oder er es für Unsinn erachtet, zu jedem Datenobjekt einfach eine (oder mehrere) simple Adapter-Klasse(n) zu bauen, in denen dann zumindest für die CRUD-Operationen SQL-Code steckt.

Das schreiben eines solchen Adapters kostet etwa 30 Minuten. Man kann dies auf 10 Sekunden verkürzen, wenn man einen kleinen Generator schreibt.

Den Code dieses Adapters kann ich danach anpassen, wie ich will. Wenn ich nur für einen Typ die Daten im Speicher cachen möchte, dann sind das ein paar Zeilen Code. Dito wenn Schreibvorgänge gesammelt werden sollen.

Alles ist an einem Ort, und mal ehrlich, das funktioniert mit jedem POCO-Objekt (ein Feature, welches die EF-Jungs groß herausstellen mussten).

Statt dessen werden Konfigurationsdetails in der n-ten Abstraktion des Frameworks m versteckt. Wir fügen eine Menge Komplexität und neue Abhängigkeiten hinzu, und unterm Strich fährt man damit noch nicht mal besser, wie das Beispiel Umbraco 5 eindrucksvoll bewiesen hat.

Coding-Dojos sind für mich ein zweischneidiges Schwert. Letztendlich dokumentieren sie ja nur die Verschiedenheit der beteiligten Menschen. Code-Reviews sind da nicht anders.

Insofern bin ich fast geneigt zu sagen, dass ein Haufen von Entwicklern sich halt gemeinsam auf Patterns einigen muss. Damit legt man den internen Maßstab fest, wie Qualität gemessen wird. Wer hinzustoßen möchte, bekommt beim Vorstellungsgespräch die simple Wahl "love it or leave it". Dann hat man auch ein Gerüst, an denen Code-Reviews dann wirklich objektiv und ohne Supervisor durchgeführt werden können.

Natürlich birgt das die Gefahr, dass derjenige mit der höchsten Hierachiestellung diese "Enticklerverfassung" oktruiert. Ob das produktiv und in der Gruppe schlau ist, darf man bezweifeln.

Anonym hat gesagt…

@McZ: Hast du das deinem Kollegen auch so geschildert? Habt ich das besprochen? Habt ihr Alternativen (und deren Auswirkungen) abgewägt?

In einer Gruppe führt systemisches Konsensieren zum allgemein akzeptablen Ergebnis.

Ich habe die Details nicht genau verstanden, aber string als Platzhalter statt einem Schema zu nutzen birgt Gefahren. Generizität, Felxibilität könnten eine Antwort sein, aber vielleicht findet man dafür eine noch bessere Lösung.

Hierarchien mögen in einer Gruppe das Meinungsbild prägen, aber das sollte nicht die Entscheidung der Gruppe sein. Hierarchie ist nicht Schlechtes, Macht zwecks Durchsetzung der Meinung ist schlechter.