Follow my new blog

Donnerstag, 17. September 2009

Root Cause Analysis einer Code Kata [endlich-clean.net]

Stefan Lieser tut es nun auch: Er hält seine Codierfinger mit Code Katas geschmeidig. Gerade hat er beschrieben, wie es ihm da bei der KataPotter ergangen ist. Zunächst ist ihm die Lösung leicht gefallen:

“Da ich es gewohnt bin, testgetrieben zu arbeiten, hatte ich mit den ersten Schritten keine Probleme. Die ersten Tests waren schnell erstellt und haben die Implementierung schnell in die richtige Richtung getrieben.”

Später war es nicht mehr so einfach:

“Dann kamen jedoch die komplizierteren Beispiele an die Reihe und ich habe länger mit der Implementierung gekämpft.”

Seine Schlussfolgerung zur Ursache mit den Lösungsschwierigkeiten:

“Ich habe erkannt, dass ich mit dem Refaktorisieren tendenziell zu früh beginne.”

Hört sich gut an, oder? So soll es sein: Erkenntnisgewinn durch Code Kata.

Da ich diese Kata neulich auch gemacht habe und auch Schwierigkeiten hatte, erlaube ich mir jedoch, hinter den Erkenntnisgewinn zu schauen. Ich bezweifle nicht, dass es einer ist und wir alle daraus etwas lernen können: TDD ist kein Selbstzweck; auch mit TDD müssen wir schauen, dass wir Nutzen produzieren, bevor wir innere Code Qualität herstellen.

Aber ich frage mich, ob Stefan damit sein Ursprungsproblem aufgedeckt hat. Hat er hier erfolgreich eine Root Cause Analysis betrieben?

Ich vermute, seine Schwierigkeit ist ein Folgeproblem eines Symptoms, das durch TDD quasi provoziert wird. Das nenne ich jetzt mal das No Design Up Front (NDUF) Symptom.

Aus meiner Sicht ist passiert, was heufig passiert und scheinbar durch TDD auch noch gutgeheißen wird: Stefan hat ein Problem gelesen, kurz darüber nachgedacht, ob er es versteht, und dann mit dem Codieren begonnen in dem Glauben, dass sich schon ein angemessenes Design bei der Codierung ergeben wird. TDD = Test Driven Design.

Der Gedanke ist sicher nicht falsch. Die Frage ist nur, wofür sich ein angemessenes Design durch die kleinen TDD-Schritte ergibt?

Meine Antwort ist: TDD führt zu einem angemessenen Design für das Modell, das man sich von einer Lösung gemacht hat. Nicht mehr, nicht weniger.

TDD ist also ein Werkzeug, das mich eine Zielvorstellung konkretisieren lässt. Mit TDD kann ich ein evolvierbares Design manifestieren. Ich schlage es mit TDD-Schritten sozusagen als Skulptur aus einem Marmorblock heraus.

Tja… was aber, wenn ich einen falschen Marmorblock gewählt habe? Wenn du zu klein ist, dann komme ich nicht zu der Skulptur, die ich gern hätte.

Verräterisch an Stefans Aussage ist, dass die  Tests “die Implementierung schnell in die richtige Richtung getrieben” haben – und er trotzdem am Ende mit den komplizierten Beispielen zu kämpfen hatte. Ich behaupte mal, Stefans anfängliche Tests bzw. die Implementierung haben eben nicht (!) in die “richtige Richtung” gezielt. Nur weil Tests grün waren, heißt das eben nicht, dass irgendwie die Gesamtlösung näher gerückt ist. Denn die Gesamtlösung enthält eben auch und gerade die komplizierten Fälle.

Mein Verdacht ist eher – und den erlaube ich mir, weil ich selbst in diese Falle getappt bin –, dass Stefan keinen Leitstern hatte und damit keine Richtung. Er hatte kein Modell der Lösung, auf das hin er Code geschrieben hat. Er hat nur unmittelbar vor seine Füße geschaut. Dort lag dann immer nur ein nächster Testfall, den er in ad hoc Manier gelöst hat.

Tut man das, dann läuft man bei der KataPotter aber unweigerlich gegen eine Mauer. Ab einem gewissen Punkt muss man mit seiner Lösungsstrategie umschwenken. Da geht es nicht mehr mit “brute force”. Das ist der Fall, wenn der beste Preis für einen Warenkorb nicht der naheliegende ist. Die Kata-Beschreibung nennt diesen Fall explizit.

Und da setzt meine Kritik an: Wider besseren Wissens ist Stefan mit Babysteps losgelaufen und hat einfach Test auf Test gehäuft. Das Problem ist dann am Ende nicht gewesen, dass er zu früh refaktorisiert hat, sondern dass er nicht vor dem ersten Schritt überlegt hat, wie die Lösung im Modell aussehen soll. Der komplizierte Warenkorb hat ihn überrascht wie den geschäftigen Familienvater alle Jahre wieder das Weihnachtsfest.

Dabei glaube ich, dass Stefan schon vor dem ersten Test ein Modell im Kopf hatte. Wenn er die Lösung für den komplizierten Warenkorb selbst gefunden hat, dann hat er auch eine implementierbare Strategie gekannt. Ich sag mal als Stichwort “Baum” ;-)

Warum hat er dann diese Strategie nicht auf einem Blatt aufgezeichnet und überlegt, welche Algorithmen und Datenstrukturen dafür nützlich wären? Warum hat er dann diese Algorithmen und Datenstrukturen nicht vom ersten Test an angepeilt? Auch das hätte kleinschrittig mit TDD geschehen können.

Stattdessen hat er sich sozusagen dümmer gestellt als er war. Er hat sich ganz TDD überlassen in dem Glauben, dass sich durch TDD schon eine Problemlösung ergeben würde. Aber TDD führt nur zu Strukturen, nicht zu Algorithmen. Die Algorithmen, die grundsätzlichen Lösungsansätze, die Modelle, die entstehen im Kopf. Sie sind die Leitsterne für das Voranschreiten mit TDD.

Da liegt für mich das Wurzelproblem. Nicht nur bei Stefan. Ich bin ja selbst in diese Falle getappt. TDD bietet sich so vollmundig als Design-Werkzeug an, dass wir (und sicher auch andere) ihm auf den Leim gehen und allzuleicht glauben, dass wir uns weitere Gedanken ersparen können. Mit TDD gleich loslegen können: das ist so verlockend.

Aber TDD ist immer nur so gut wie das Modell, das ich für eine Lösung habe. TDD ist ein Werkzeug, das mir den Weg zu wartbarem Code für ein gegebenes Modell zeigt. TDD ersetzt die Modellierung aber nicht. Und die lohnt sich eben – wie hier zu sehen ist – auch für ein so kleines Problem wie die KataPotter. Mich lehren meine eigenen und nun auch Stefans Schwierigkeiten deshalb, dass sich ein paar Gedanken zur Lösung immer lohnen. Die Lösung sollte ich im Kopf haben – aber nicht ihre Struktur. Zu der führt mich TDD.

Kommentare:

Anonym hat gesagt…

Hallo,

"TDD ist nur so gut wie das Modell selbst". Dem stimme ich zu, aber aus einem vielleicht etwas überraschenden Blickwinkel.

Um einen "puren" Algorithmus zu modellieren, gibt es eine ausgezeichnete Modellierungsmethode: Programmiersprachen! Eine "höhere" Modellierungsmethode zu verwenden ergibt also keinen wirklichen Sinn. Daher scheitert, meiner Meinung nach, der Versuch des TDD.

Einen Algorithmus zu testen, indem man die erwarteten Ergebnisse mit eben diesem Algorithmus ausrechnet, erscheint mir nicht sinnvoll. Die einzig wirklich gute Methode besteht, z.B. bei numerischen Algorithemen, in einem Test mit analytisch berechenbaren Ergebnisses.

Im Falle dieser Kata bleibt einem nichts weiter übrig, als sich mit einigen wenigen Ergebniswerten davon zu überzeugen, dass der Code "im Wesentlichen" richtig ist (d.h. keine elementaren Fehler aufweist) evtl. angereichert mit einer Code-Inspektion. Weiterführende Tests sind meiner Meinung nach nicht sinnvoll.

Ralf Westphal - One Man Think Tank hat gesagt…

@Anonym: Ich würde das, was ich mit einer Programmiersprache mache, wenn ich einen Algorithmus implementiere, nicht als Modellierung bezeichnen.

Ein Baum oder ein endl. Automat: das können Modelle sein. Die implementiere ich dann mit einer Programmiersprache.

Wenn ein Modell nicht "höher" liegt als das Gebiet, für das ich eine Lösung suche, dann ist was im Argen, finde ich. Dann lohnt das "Modell" nicht.

-Ralf

Kai Beck hat gesagt…

Hallo Ralf,

gestern ist mir ein Flyer in die Hände gefallen. Unter der Überschrift Clean Code Developer stand da doch glatt Test Driven DESIGN. Nach diesem Blogartikel hätte ich für die Abkürzung TDD eher Test Driven DEVELOPMENT dort erwartet.

Mir ging es bei den ersten Katas ganz ähnlich wie Stefan und Dir. Mit TDD bin ich zu einer gekapselten, testfähigen Lösung gekommen. Aber erst in den folgenden Wiederholungen des gleichen Katas gefiel mir das Design und die Testfälle wirklich gut. Das lag daran, dass ich die Problemdomäne verinnerlicht und die Anforderungen viel besser verstanden habe.

In echten Entwicklungsprojekten kann ich eine Aufgabe nicht beliebig oft wiederholen. Da hilft es, wenn ich mir vor dem ersten Test u.a. Gedanken über die Schnittstellendefinition mache. Wenn ich dabei schon ins Strudeln gerate, ist das ein Indikator für unklare Anforderungen. Sind die geklärt und vielleicht sogar schon in signifikante User Stories aufgebrochen worden, klappts mit TDD um so schneller.

Kai