Follow my new blog

Donnerstag, 26. August 2010

Präzisierung eines Rahmens für TDD

TDD ist nur ein Tool. Deshalb kann man TDD nicht nur besser oder schlechter bedienen, sondern auch angemessen oder unangemessen benutzen. Gestern beim 4. Coding Dojo in Hamburg gab es für beides Beispiele. Das war schön, denn so konnten wir etwas lernen.

Über die “Bedienung” des TDD-Werkzeugs ist anderswo schon einiges geschrieben worden. Dazu gehört die Einhaltung des Red-Green[-Refactor]-Rhythmus oder die Bennung von Tests oder die Zahl der Asserts pro Test usw. Deshalb an dieser Stelle heute keine Bemerkungen dazu.

Die Angemessenheit findet allerdings aus meiner Sicht noch keine angemessene Beachtung. Wann ist TDD angemessen? Wann nutzen und wann nicht? In einem früheren Posting hatte ich mir dazu schonmal Gedanken gemacht. Von dem dortigen Rahmen für TDD bin ich weiterhin überzeugt:

  1. Bevor Sie in die Tasten greifen und einen Test schreiben, sollten Sie sicherstellen, dass Sie das Problem durchdrungen haben.
  2. Auch nach Durchdringung des Problems sollten Sie nich sofort loscodieren, sondern erst einen Lösungsansatz entwerfen.

Im 4. Coding Dojo in Hamburg wurde nun gestern eine Kata fortgeführt (KataPokerHands), die beim 3. Dojo angefangen worden war. Bei dem bin ich nicht dabei gewesen, insofern kann ich nicht genau sagen, inwiefern sich die Gruppe in diesem Rahmen bewegt hat. Sicherlich hat man aber versucht, die Anforderungen zu verstehen. Und im Code waren auch Effekte von Nachdenken zu sehen. Dennoch bewahrheitete sich recht schnell die schon am Ende des 3. Dojos geäußerte Vermutung, dass die Lösung suboptimal sei.

Meine Erkenntnis nach dem 4. Dojo ist, dass der bisherige Rahmen noch zu unpräzise ist. Er gibt am Ende der Implementation keine Richtung. Was tun mit dem Verständnis des Problems und dem Lösungsansatz? Wo und wie beginnen mit der Umsetzung des Lösungsansatzes?

TDD beginnt beim API

Ergebnis des Lösungsentwurfs ist eine Menge von Funktionseinheiten, die zusammen die Anforderungen umsetzen. Je nach Ausdauer beim Nachdenken über den Lösungsansatz sehen Lösungsansätze natürlich unterschiedlich aus. Das 3. Dojo ist für die KataPokerHands auf diese Funktionseinheiten gekommen:

image

Mich haben 30 Sekunden Nachdenken hingegen zu einer größeren Menge Funktionseinheiten gebracht:

image

Ich will nicht sagen, dass der eine oder andere Lösungsansatz besser ist. Hier gehts nur darum, dass die Funktionseinheiten des Lösungsansatzes auf unterschiedlicher Ebene liegen können. Manche sind “von außen” sichtbar – Hand oder Pokerspiel entscheiden –, manche nicht.

Mit “von außen” meine ich, dass ein Nutzer der Lösung sie sieht, mit ihnen umgehen muss. Für Blatt bewerten in meinem Ansatz gilt das z.B. nicht; es ist eine interne Funktionseinheit, mit der ich mir die Implementation leichter machen will. Ich muss nicht erst durch TDD auf sie stoßen und durch Refaktorisierung freistellen. So mag ich es – andere ziehen einen anderen Weg vor.

Nun hat also eine Entwurfsphase einen Lösungsansatz mit einigen Funktionseinheiten geliefert. Wie sollte TDD darauf nun ansetzen? Immer beim API.

TDD sollte sich initial immer nur um die Member kümmern, die zum API einer Funktionseinheit gehören. Das sind die Member, die für die Nutzer einer Funktionseinheit relevant sind.

Hier hatte das 3. Dojo aus meiner Sicht einen Fehler begangen bei allem guten Willen zu TDD. Damit hat man eine Chance verschenkt, durch TDD ein gutes Design “austreiben” zu lassen.

Damit Sie verstehen, was ich beobachtet habe, hier die Anforderung der Kata in Kürzestform:

Vergleiche zwei Pokerblätter und bestimme das Gewinnerblatt

Das 3. Dojo hatte sich dafür entschieden, ein Pokerblatt durch eine Instanz der Klasse Hand zu repräsentieren:

class Hand
{
…       
}

Und dann? Dann hatte man angefangen, Tests zu schreiben für Properties, die anzeigen, ob ein Blatt der einen oder anderen Poker-Figur (Pair, Full House usw.) entspricht. Die Klasse wurde schnell mit getestetem Code gefüllt.

class Hand
{
    public bool IsPair { get {...} }
    public bool IsThreeOfAKind { get {...} }
    public bool IsFourOfAKind { get {...} }

}

Die Tests waren schön grün. Aber war das Design auch gut? Nein, nicht wirklich. Das hatte die Gruppe dann auch schon am Ende des 3. Dojos erkannt und dieser Eindruck wurde beim 4. Dojo, das den Code weiterentwickeln sollte, bestätigt.


Ich glaube nun, der Grund dafür, dass trotz TDD kein gute Ergebnis herausgekommen war, liegt daran, dass die Tests nicht beim API, sondern bei imaginierten Interna angesetzt haben. Aus der Aufgabenstellung geht einfach nicht hervor, ob eine IsPair Property o.ä. benötigt wird. IsPair gehört nicht zum API, weil es den Nutzer der Klasse Hand nicht interessiert. Der will nur wissen, welches von zwei Hand-Objekten der Gewinner ist.


Beim 4. Dojo kam man gleich zu Beginn auf diese Frage und entschied sich, bei allen Zweifeln am Design doch den bisherigen objektorientierten Ansatz weiter zu verfolgen. Das Ergebnis sah dann so aus:

class Hand : IComparable<Hand>
{
    public int CompareTo(Hand other)
    {
        …
    }
}
Hand h1, h2;

if (h1.CompareTo(h2) > 1)
    Console.WriteLine("h1 gewinnt");

Ein Nutzer stellt Blätter in Hand-Objekten zusammen und vergleicht sie über die Standardschnittstelle IComparable<T>.


Das ist doch ein wunderbar objektorientierter API, oder?


Leider hatte das 3. Dojo nicht darüber nachgedacht oder zumindest nichts daraus gemacht. Denn hätte es seinen ersten Test gegen diesen API geschrieben, wäre die weitere Entwicklung sicherlich anders verlaufen.


Beim Lösungsansatz des 3. Dojo gab es nur eine wesentliche Funktionseinheit und die deshalb auch dem Nutzer der Lösung sichtbar war. Ihr API fiel zusammen mit der Sicht des Nutzers.


Mein Lösungsansatz hat hingegen nicht nur für den Nutzer sichtbare Funktionseinheiten. Was ist denn da der API z.B. für Blatt bewerten? Das finden Sie heraus, wenn Sie sich fragen, wer die Nutzer der Funktionseinheit sind. Das könnte z.B. die umschließende Funktionseinheit Pokerspiel bewerten sein. Was könnte die von Blatt bewerten wollen? Ich denke, in diesem Fall ist es so einfach wie: “Ich will Blatt bewerten mit einem Blatt aufrufen und eine Blattbewertung als Ergebnis bekommen.”


Ich würde mich daher für eine Übersetzung von Blatt bewerten in eine Funktion entscheiden. Dasselbe gilt für die Funktionseinheit Gewinner ermitteln.

class Blatt
{
   
}
   

public class PokerspielBewerten
{
    internal class Blattbewertung : IComparable<Blattbewertung>
    {
       
        public int CompareTo(Blattbewertung other)
        {
           
        }
    }


    public enum Gewinner
    {
        Schwarz,
        Weiß,
        Patt
    }


    internal Blattbewertung BlattBewerten(Blatt blatt)
    {
       
    }


    public Gewinner GewinnerErmitteln(Blatt schwarz, Blatt weiß)
    {
        Blattbewertung bewertungSchwarz = BlattBewerten(schwarz);
        Blattbewertung bewertungWeiß = BlattBewerten(weiß);

        switch(bewertungSchwarz.CompareTo(bewertungWeiß))
        {
            case -1: return Gewinner.Weiß;
            case 1: return Gewinner.Schwarz;
            default: return Gewinner.Patt;
        }
    }
}

In diesem Fall sind die APIs der nicht öffentlichen Funktionseinheiten simpel. In anderen Fällen müssen Sie etwas länger nachdenken. Immer jedoch geht es um die minimale Zahl an Members einer Funktionseinheit, die für ihre unmittelbare Umgebung, ihre Nutzer sichtbar sein müssen. Ausschließlich darauf sollte TDD ansetzen.


Und wie komme ich bei meinem Lösungsansatz darauf, dass es überhaupt eine Funktionseinheit Blatt bewerten geben sollte? Nachdenken hilft ;-) Aber natürlich sollte das nicht zu spekulativ sein. Festzustellen, ab wann es spekulativ wird, ist einerseits Erfahrungssache, andererseits Geschmackssache. Und selbstverständlich sind Sie hinterher immer schlauer; sie können sich also auch vertun und zuwenig oder zuviel nachdenken. Das passiert. Es deshalb jedoch zu lassen, verschenkt Produktivitätspotenzial.


Ohne Nutzen nützt TDD nichts


Mit TDD konsequent nur auf dem API von Funktionseinheiten zu beginnen, ist ein Erfolgsfaktor. Bis Sie damit Erfolg haben, kann es jedoch dauern. Deshalb sollten Sie ebenfalls beachten: Produzieren Sie so schnell wie möglich Nutzen.


Auch hier hatte das 3. Dojo “gesündigt”. Nach mehreren Stunden der Arbeit konnte der Code zwar eine Menge und alle Tests waren grün – nur wäre keine Auslieferung des Codes möglich gewesen. Es gab keinen API, es gab nichts für einen Nutzer Nützliches. Die so entscheidende Vergleichsfunktionalität für Hand-Objekte gab es nicht.


Hätte das 3. Dojo mit Tests durch den API begonnen, wäre das Problem nicht vorhanden oder zumindest nicht so groß gewesen. Doch auch dann wäre die Marschrichtung noch nicht sonnenklar gewesen. Wie hätte man entwickeln sollen? Zuerst alle Figuren erkennen und vergleichen können? Oder nur wenige Figuren, dafür aber die auch noch nach ihrem Kartenwert unterscheiden können (ein 2-Pair ist weniger wert als ein König-Pair)?


Diese Frage stellt sich noch deutlicher, wenn Ihr Lösungsansatz mehrere Funktionseinheiten enthält. Sollte ich zuerst Blatt bewerten komplett implementieren, bevor ich mich an Gewinner ermitteln machen?


Suchen Sie die Antwort immer beim Kundennutzen. Stellen Sie sich vor, dass der Kunde nach dem nächsten grünen Test die Software ausprobieren möchte. Kann er das? Haben Sie mit dem letzten red-green-Zyklus etwas geschaffen, das für den Kunden einen Unterschied macht?


Implementieren Sie also immer in Durchstichen. Bei mehreren Funktionseinheiten sollten Sie sich also nicht an internen festbeißen. Übertragen auf meinen Lösungsansatz hatte das 3. Dojo seine ganze Energie in Blatt bewerten investiert. Es hätte nicht ausliefern können. Ohne Gewinner ermitteln wäre für den Kunden/Nutzer kein Wert erkennbar gewesen.


Darüber hinaus hatte dsa 3. Dojo aber auch unabhängig von den Funktionseinheiten den Fokus auf der Implementation der Figurenerkennung. Die ist ber nur für einen Teil der Gewinnermittlung relevant. Bei gleicher Figur geben die Kartenwerte den Ausschlag. Und bei gleichen Kartenwerten die Bewertung der nicht zur Figur gehörenden Karten.


Diese weiteren Bewertungen waren nicht angegangen worden. Man hatte also keinen Eindruck davon, ob sie einfach oder kompliziert durchzuführen wären. Vielleicht wären sie ja sogar entwurfsrelevant gewesen?


Teilen Sie Anforderungen in Äquivalenzklassen und realisieren aus jeder zunächst nur ein Feature. Die Äquivalenzklassen für das Pokerspiel sind aus meiner Sicht:



  • Karten haben eine Farbe
  • Karten haben einen Wert
  • Gewinnerkennung aufgrund Figur
  • Gewinnerkennung bei gleicher Figur nach Kartenwert
  • Gewinnerkennung bei gleicher Figur und gleichem Kartenwert nach Restkartenwert

Es hätte dann genügt, Karten mit zwei Farben und Werten zu erkennen. Und es hätte vor allem genügt, nur Zwillinge und eine kleine Straße als Figuren zu erkennen, aber auch deren Kartenwert und Restkartenwert zu bestimmen.


Ein imaginierter Kunde hätte dann schon ganz verschiedene Aspekte eines Pokerspiels ausprobieren können. Das hätte geholfen herauszufinden, ob die Anforderungen verstanden wurden. Und das hätte geholfen, das Design möglichst früh auszutreiben bzw. zu verifizieren.


Fazit


TDD is the way to go beim Unit Testing. Aber nicht grüne Tests allein oder der Rhythmus red-green[-refactor] sind ausreichend. Softwareentwicklung und Testen sind kein Selbstzweck. Denken Sie daher immer an den Nutzer des System under Test. Beginnen Sie Ihre Tests beim API der entworfenen Funktionseinheiten; starten Sie also mit Black Box Tests Ihrer Systems under Test. (Was nicht heißt, dass TDD bedeutet, sie sollten nur Black Box Tests schreiben; Funktionseinheiten, die TDD austreibt, können sehr wohl unsichtbar für einen SUT-Nutzer sein. D.h. auch White Box Tests sind völlig ok mit TDD, nein, sie sind am Ende sogar eigentlich immer nötig.)


Produzieren Sie dann so schnell und so vielfältig Kundennutzen in möglichst vielen Bereichen.


Selbst wenn Sie nicht viel von großartiger Architektur halten, tun Sie Ihrer TDD Praxis einen gefallen, wenn Sie immer den Kunden im Blick haben. Auch nicht zu verachten ist, dass kleine Nutzeninkremente und quasi ständige Auslieferfähigkeit Ihnen ein gutes Gefühl machen.

5 Kommentare:

Anonym hat gesagt…

Hallo Ralf,

vielleicht wäre BDD das richtige Framework zur testgetriebenen Entwicklung gewesen, weil dadurch auch verschiedene Szenarien im Vorfeld ("eine Karte", "zwei Karten") dargestellt werden könnten. Der Kunde kann danach (bei grün) sofort selber ausprobieren, ob die Anwendung seinen Wünschen entspricht.

Allerdings stellt sich dann auch die Frage, ob StoryQ bspw. im Rahmen eines Dojos nicht zu techniklastig ist.

Viele Grüße,
André

Anonym hat gesagt…

Hallo Ralf,

Ihren, zugegebener Maßen sehr umfangreich und ausführlich dargelegten Gedanken zufolge, müsste man das Dojo in ein (Architektur &) Design Treffen und ein TDD Dojo teilen?
Natürlich wurden im 3. Dojo einige, teils gewichtige (teure) Fehler gemacht.
Natürlich verlangt jede SW-Entwicklungsaufgabe eine zu ihrer Komplexität angemessene Durchdringung.
Ich gehe davon aus, dass alle Teilnehmer gestern Abend neue Erkenntnisse gewonnen haben. Für meinen Teil bekam ich die (triviale?) Bestätigung, dass man vor dem Angehen einer Aufgabe eine hinreichende Durchdringung derselben haben muss. Ob das allerdings gleich ein „komplettes“ Design sein muss, darf aus Sicht des TDD bezweifelt werden. Wenn das von Ihnen oft zitierte Nutzer-API bestimmt ist, kann bereits losgelegt werden, oder? Enthusiasten meinen ja, dass TDD selbst Design austreiben kann und immerhin erzeugt man dann schon (nutzbringenden) Kode, anstatt sich möglicherweise in Design zu verlieren. (Nicht immer und nicht jeder hat bereits nach 30 Sekunden eine Design-Lösung parat.)
Fazit: Etwas mehr Augenmerk auf die Bestimmung des Nutzer-API zu Beginn. Ggf. auch zeitige „Durchstiche“, um das sich entwickelnde Design auszuleuchten. Ansonsten aber striktes Festhalten an TDD, denn genau darum geht es in diesem Dojo. Und insbesondere „durchstichige“ Refaktorierungen mit YAGNI/KISS-Smell vermeiden.

Grüße,

Ihr CoPilot

Ralf Westphal - One Man Think Tank hat gesagt…

@Copilot: API als Ausgangspunkt und Denken in Durchstichen - ja, ich würd sagen, dass ich das allermindeste, was man zu TDD noch hinzunehmen muss, damit es klappt.

Zwar bin ich davon überzeugt, dass weiteres Nachdenken auch sehr hilfreich ist... Aber darüber können wir ein andermal sprechen.

(Sitze grad bei einem Kunden, der zur Übung ein internes Dojo macht. Da wurde z.B. auch zuviel nachgedacht. Bringt dann auch nix.)

-Ralf

Anonym hat gesagt…

Hallo Ralf,

Deine Funktion BlattBewertung BlattBewerten(Blatt blatt) braucht ja auch eine öffentliche Schnittstelle für das Blatt.
Hast Du nicht dasselbe Problem wieder, dass Du am Blatt Methoden
publizieren musst, die das Pokergame nicht braucht?

Markus

Ralf Westphal - One Man Think Tank hat gesagt…

@Markus: Das Blatt ist nur eine Datenstruktur. Die ist nicht intelligent. Oder muss es zumindest nicht sein. So trenne ich Logik von Datenstruktur. Ist für mich eine saubere Separation of Concerns.