Montag, 22. März 2010

TDD, aber bitte mit System

Hier sind zwei recht ordentliche Artikel zum Thema TDD: Teil 1 und Teil 2. Mir gefällt daran besonders, dass Neal Ford sein TDD beginnt mit einer kleinen Liste von Schritten auf dem Weg zur Beantwortung der Frage, ob eine Zahl eine Vollkommene Zahl ist.

Bei allem Gefallen an seinen Artikeln reibe ich mich jedoch an einigen Punkten.

Fragliche Objektorientierung

Wenn Sie die Wurzel einer Zahl berechnen wollten und ich würde Ihnen sagen, das geht mit C# so:

var sqc = new SqrtCalculator(7);
Console.WriteLine(“Wurzel aus 7: {0}”, sqc.Calculate());

Würden Sie das als naheliegend empfinden? Kaum.

Oder wenn ich Ihnen anbieten würde, einen String bei einer Zeichenkette so zu splitten:

var splitter = new StringSplitter(“ ”);
var words = splitter.Split(“the quick brown fox”);

Würden sie das als komfortable ansehen? Kaum.

Nichts anderes schlägt Neal Ford aber vor, wenn er seine Implementation zur Prüfung von Zahlen auf Vollkommenheit so aussehen lässt:

var c = new Classifier(6);
Console.WriteLine(“Ist 6 vollkommen: {0}”, c.IsPerfect());

Das ist genausowenig komfortabel oder naheliegend wie die obigen Beispiele. Das ist erzwungen objektorientiert. Das ist ein für den Anwender der Funktionalität nicht nachvollziehbarer API.

Die Prüfung, ob eine Zahl vollkommen ist, ist eine Funktion genauso wie die Prüfung, ob eine Zahl gerade ist oder die Berechnung ihrer Wurzel. Warum also sollte ich als Nutzer einer solchen Funktion(alität) eine Klasse instanzieren müssen? Warum sollte diese Klasse auch noch zustandsbehaftet sein?

Neil leitet das auf Seite 8 seines Teil 1 daraus ab, dass zwei interne Funktionen (factorsFor(int number) und isFactor(int number)) auf demselben Parameter arbeiten, der zu prüfenden Zahl. Er meint, das sei nun wirklich zuviel prozedurale Programmierung. Oder vielleicht scheut er sich vor der “Feuchtigkeit” der Wiederholung eines Parameters? Ich weiß es nicht. Ich finde es einfach nur überkandidelt – und zwar leider in einer Weise die symptomatisch ist für die Branche. Denn sein Vorgehen ist ja tragisch: Er will das Gute erreichen, verschlimmert die Situation aber. Er möchte einem Paradigma dienen, das für sich gepachtet zu haben scheint, dass es durch und durch gut sei – aber das Paradigma passt leider nicht zum Problem. Mit dem Hammer Objektorientierung wird die funktionale Schraube eingeschlagen. Autsch!

Hier dagegen meine Lösung:

public class VollkommeneZahlen

{

    public static bool IstVollkommen(int zahl)

    {

        return EchteTeilerVon(zahl)

              .ErgebenEineVollkommeneZahl(zahl);

    }

    …

Die Nutzung sieht so aus:

Console.WriteLine(VollkommeneZahlen.IstVollkommen(6));

Das halte ich sowohl in der Anwendung wie auch in der Implementation für intuitiv und angemessen. Egal, ob das nun speziell objektorientiert oder sonstwas ist. Der Anwender versteht es, weil eine Funktion als Funktion realisiert ist. Und der, der den Code liest, sieht sofort, wie das Ergebnis in zwei Schritten zustande kommt.

Unklare Verteilung von Verantwortung

Nicht nur ist aber die Objektorientierung für das Problem unangemessen, wie ich finde. Sie hat sogar einen negativen Effekt auf die Verständlichkeit und Flexibilität des Codes.

Vollkommene Zahlen sind solche, deren echte Teiler in Summe die Zahl ergeben. Die Teiler von 6 sind 1, 2, 3 und 6. Allerdings ist 6 kein echter Teiler. Die Summe ist deshalb nur aus 1, 2 und 3 zu bilden und die ergibt 6. Damit ist 6 eine Vollkommene Zahl.

In Neal Fords Code ist die besondere Behandlung der Zahl selbst als unechter Teiler an zwei Stellen codiert:

image

Er fügt die Zahl selbst als Teiler zunächst der Menge aller Teiler hinzu – und zieht sie am Ende wieder ab. Damit erfüllt auch seine Funktion calculateFactors() nicht mehr wirklich ihre Verantwortlichkeit, die Teiler der Zahl zu berechnen. Das trägt nicht zur Verständlichkeit der Algorithmusimplementierung bei.

Außerdem ist isPerfect() nicht nur von der Zahl als Zustand abhängig, sondern auch noch indirekt von der Menge der Teiler, die calculateFactors() berechnet und sumOfFactors() auswertet. Solcher Zustand erhöht immer die Komplexität einer Lösung. Und er macht die Wiederverwendung von Methoden in anderen Zusammenhängen schwieriger.

Meine Funktion IEnumerable<int> EchteTeilerVon(int zahl) hingegen ist von nichts abhängig und kann daher in jedem Zusammenhang, in dem nur die echten Teiler einer Zahl relevant sind, wiederverwendet werden.

Auch macht bool ErgebenEineVollkomeneZahl(this IEnumerable<int> teiler, int zahl) den Code selbstdokumentierender. Das Abstraktionsniveau der Bestandteile von IstVollkommen() ist einheitlich. Bei Neal hingegen liegen calculateFactors() und der Ausdruck zur Berechnung des Rückgabewertes auf unterschiedlichen Abstraktionsniveaus. Der Leser muss sich zusammenreimen, dass eine Zahl vollkommen ist, wenn die Summe von Teilern abzüglich einer Zahl gleich der Zahl ist. Zugegeben, das ist nicht so kompliziert, doch es ist ein spürbarer intellektueller Aufwand, der nicht Not tut. (Insbesondere verwunderlich ist sein Ansatz, da er in einem anderen Artikel das Single Level of Abstraction Prinzip lobt.)

Gesucht: Klares Vorgehen

Schließlich vermisse ich bei Neals Artikeln, dass er sein Vorgehen nicht weiter deutlich sichtbar systematisiert. Er tut schon das Richtige, in dem nicht mit TDD “reinspringt” und als erstes einen Test für isPerfect() schreibt, sondern mal einen Gedanken an den Algorithmus verschwendet. Er denkt also nach, bevor er codiert. Doch das hebt er nicht hervor. Einzig der “TDD workflow” wird als Handlauf für das Vorgehen wieder einmal thematisiert.

Schade. Denn zu TDD gehört mehr, denke ich. Hier mein Version eines Entwicklungsprozesses, in den TDD eingebettet sein sollte:

  1. Verstehen
  2. Nachdenken/planen
  3. Unsicherheiten ausräumen
  4. Codieren

Diese Schritte beziehen sich auf ein Problem bzw. auf die das Problem lösende Funktionseinheit. Sie sind daher rekursiv zu durchlaufen, sollte die Funktionseinheit in weitere zerfallen. Das ist ein wichtiger Punkt! Denn auf Zerlegungsebene eines Problem gilt es zuerst zu verstehen, dann zu planen usw.

Zu 1: Am Anfang der Entwicklung einer Softwarelösung – sei es Methode oder Klasse oder Komponente oder Anwendung – steht das Verstehen. Investieren Sie Zeit, das Problem oder gar den Kunden zu verstehen. Machen Sie sich klar, was die Anforderungen wirklich sind. Tragen Sie Fälle zusammen, die beschreiben, was die Eingaben/Parameter und der zugehörigen erwarteten Ausgaben/Ergebnisse sind. Das hört sich selbstverständlich an. In der Praxis ist aber schwierig und bedarf immer wieder Mut und Disziplin. Beide werden oft nicht ausgebracht. Schöner ist es, schnell den Code Colt zu ziehen und zu programmieren.

Leider ist ungenügendes Verständnis aber wohl eine der wesentlichen Ursachen für soviele Probleme der Softwareentwicklung. Wer ungenügend versteht, der implementiert zuviel. Wer ungenügend versteht, der implementiert das falsche oder inkorrekt.

Letztlich ist ungenügendes Verständnis zwar nie ganz zu vermeiden, doch ein wenig mehr Mühe darf es schon sein. Vor allem ist in den Prozess des Verständnisaufbaus der Kunde sehr aktiv mit einzubeziehen. Fragen Sie ihm Löcher in den Bauch! Dafür ist er da ;-) Lassen Sie ihn nicht aus seiner Verantwortung. Wenn er etwas von Ihnen will, dann soll er wirklich so genau wie möglich beschreiben, was das ist, wie es aussehen soll, wie es sich verhalten soll. Erbitten Sie von ihm sehr konkrete Abnahmetests.

Falls der Kunde sich bei solch “peinlichen Befragung” ziehrt, machen Sie ihm klar, dass das Ergebnis Ihrer Arbeit nur so gut sein kann wie seine Spezifikation. Garbage in, garbage out – das gilt auch hier. Wer nur ungenau spezifiziert, der kann auch nur ein ungenaues Ergebnis bekommen. Dann muss nachgebessert werden. Das macht niemandem Freude.

Da die meisten Kunden mit sehr genauer Spezifikation überfordert sein werden, gibt Ihnen das die Chance, ein agiles Vorgehensmodell “zu verkaufen”. Nutzen Sie die Chance! Aber nehmen Sie das nicht als Entschuldigung, ohne gründliches Verständnis mit dem Codieren zu beginnen.

Zu 2: Wenn Sie meinen, das Problem durchdrungen zu haben, sollten Sie noch nicht zur Tastatur greifen. Auch nicht, wenn Sie TDD betreiben wollen. Tun Sie sich den Gefallen und denken Sie zuerst über einen Lösungsansatz nach. Diese Phase fällt leider immer wieder zu kurz aus. Auch in wohlmeinenden Coding Dojos wird das nicht unbedingt praktiziert. TDD als Test-Driven Design verstanden soll es richten. Codieren kann Planung und Nachdenken aber nicht ersetzen, sondern allenfalls unterstützen. Eine Struktur für Ihre Software ergibt sich nicht einfach. Die will bewusst entworfen werden.

Das ist, was mir an Neals Artikel gefallen hat: Er hat über das Problem nachgedacht und einen kleinen Plan aufgestellt. Er ist auf zumindest drei Funktionseinheiten/Verantwortlichkeiten gekommen, aus denen eine Lösung für das Problem “Vollkommene Zahlen erkennen” besteht:

  • Potenzielle Teiler einer Zahl erzeugen
  • Feststellen, ob eine potenzieller Teiler tatsächlich ein Teiler ist
  • Aufsummieren der tatsächlichen Teiler, um festzustellen, ob sie die Zahl ergeben

Das sind zugegeben kleine Funktionseinheiten. Macht aber nichts. Jede, die Sie beim Nachdenken finden, ist eine gute. Denn damit bekommen Sie “Bausteine” an die Hand, die sie separat testen können. Das ist immer gut. Und Sie können womöglich entscheiden, in welcher Reihenfolge Sie deren Umsetzung angehen.

Ein zunächst monolithisches Problem zerfällt so in kleinere Probleme und die womöglich wiederum in kleinere usw. Nachdenken hilft also bei der Komplexitätsbewältigung.

Zu 3: Allerdings mag es sich herausstellen, dass Sie sich mit der Umsetzung der einen oder anderen Funktionseinheit, auf die Sie beim Nachdenken gestoßen sind, nicht 100%ig wohlfühlen. Das ist ganz normal. Es mag an der Problemdomäne liegen oder an einer Technologie, die zum Einsatz kommen soll. Deshalb ist es wichtig, dass Sie vor dem Codieren noch einen Zwischenschritt machen.

Seien Sie sensibel für Ihre Unsicherheiten und räumen Sie sie aus. Das können Sie durch das Studium von Fachliteratur tun. Oder Sie befragen den Kunden nochmal. Oder Sie programmieren etwas. Rotzen Sie Code raus (Spike Solution), um sich z.B. mit dem neuen O/R Mapper vertraut zu machen, bevor Sie damit Produktionscode schreiben. Oder für das Problem der vollkommenen Zahlen könnten Sie sich Erweiterungsmethoden anschauen, die es möglich machen, die Funktion IstVollkommen() so lesbar zu gestalten.

Solange Sie noch unsicher sind, sollten Sie nicht mit der Codierung beginnen. Ansonsten entsteht schnell akzidenzielle Komplexität, d.h. Komplexität, die nicht nötig ist, die Sie letztlich auch nicht wollen.

Wenn Sie etwas programmieren, um Ihre Unsicherheit abzubauen, dann schmeißen Sie es am Ende besser weg. Nehmen Sie Erkenntnisse mit ins Codieren, aber keinen Code. Auch sind solche Spike Solutions keine Prototypen. (Allerdings können Sie offizielle Prototypen natürlich immer noch mit dem Kunden vereinbaren. Dann teilen Sie Ihre Unsicherheit mit dem Kunden.)

Zu 4: Erst wenn Sie genau verstehen, was der Kunde braucht, einen Plan haben, wie Sie ihm das geben können, nicht mehr unsicher sind, erst dann sollten Sie mit dem Codieren beginnen. Das gilt aus meiner Sicht auch für TDD. Oder gerade für TDD! Denn immer wieder sehe ich bei Entwicklern, die mit TDD beginnen, dass sie Schwierigkeiten haben, sich zuerst Tests vorzustellen, bevor sie etwas implementiert haben.

Das liegt meiner Meinung nach daran, dass sie das Problem noch nicht genügend gut durchdrungen haben bzw. der Kunde keine Akzeptanztestfälle geliefert hat. Und/oder es liegt daran, dass sie unsicher sind, was eigentlich zu implementieren ist. Denn wer davon keine Vorstellung hat, der tut sich schwer damit, Erwartungen zu formulieren.

Der Effekt ist dann oft, dass TDD mit trivialen Tests oder Sonderfalltests begonnen wird. Die für den Kunden viel wichtigeren “happy day” Szenarien werden dadurch auf die Lange Bank geschoben. Klarheit für deren Implementation ergibt sich auch nicht aus solchen Tests. Geschäftig mit Tests zu beginnen kann also auch ein subtiles Symptom von Prokrastination sein.

Zusammenfassung

TDD ist eine zentrale Praktik für professionelle Softwareentwicklung. Gerade deshalb ist es nötig, sich ihr ganz bewusst zu sein und immer wieder zu fragen, ob sie schon optimal betrieben wird. Ein systematisches Vorgehen hilft dabei – gerade am Anfang.

Vor dem falschen Programmierparadigma schützt TDD aber natürlich nicht. Wir tun also gut daran, auch seine Grenzen bewusst zu sehen. TDD produziert also immer nur Code auf dem fachlichen Wissensstand seines Anwenders. Deshalb ist es gut, immer wieder über den Tellerrand (der Objektorientierung) zu schauen und sich mit anderen auszutauschen.

5 Kommentare:

Rainer Hilmer hat gesagt…

Hallo Ralf,
als ich deine Vorschläge las, dachte ich, "das ist doch selbstverständlich". Na ich hab mich wohl getäuscht.
Zu deinem Punkt 4 möchte ich noch einen Gedanken, eine Erfahrung in den Raum werfen: Kunden wissen auch nicht immer im Detail was sie wollen; haben eher nebulöse Vorstellungen. Ich erinnere an "Customers from hell". Wenn man als Entwickler mit solchen Vorgaben arbeiten muß - tja was macht man da am besten?
Gruß
Rainer

Ralf Westphal - One Man Think Tank hat gesagt…

@Rainer: Nein, ich denke nicht, dass das, was ich beschrieben habe, selbstverständlich ist.

Deine Frage, "Was tun, wenn man es mit Customers from Hell zu tun hat?" finde ich sehr verständlich. Die Antwort liegt für mich allerdings ganz klar auf der Hand. Sie ist total simpel:

Wer nicht weiß, was er will, kann nicht bekommen, was er will.

So einfach ist das.

Wer nicht weiß, was er will, muss mit den Entscheidungen anderer leben. Dass das schwierig sein kann, weiß jeder, der mal einem Kind ein Eis gekauft hat, das sich nicht entscheiden konnte. "Willst du Erdbeer oder Schoko?", "Weiß nicht.", "Ok, hier ist Schoko.", "Bääääähhhh, ich wollte Erdbeer." :-)

Kunden, auch wenn Jahrzehnte älter, sind da nicht besser. Die tragen dazu allerdings einen Anzug und keine Latzhose :-)

Wenn der Kunde nicht weiß, was er will, dann musst du ihm genau das ganz, ganz, ganz klar machen. Ohne Vorwurf. Denn es ist nicht Schlimmes, nicht genau zu wissen, was man will. Es ist bei der "Bestellung" einer Software sogar fast unvermeidlich.

Deshalb ist es zentral, dem Kunden das zu erklären. Denn dann kann er Vorkehrungen treffen.

Die Unentschiedenheit/Unwissenheit eines Kunden als Entwickler kompensieren zu wollen, grenzt ans Übermenschliche. Das kann nicht funktionieren. Das führt in den Frust oder gar in den Burnout.

Unwissen und Unentschiedenheit müssen mit einem passenden Vorgehensmodell beantwortet werden. Eines, das Wissen und Entscheidungen fordert, ist da offensichtlich untauglich.

Es hilft nur 1. offene Kommunikation über die Unentschiedenheiten/das Unwissen, 2. die iterative Annäherung an etwas, das der Kunde als ok empfindet.

Zum Schluss: Was tun, wenn du mit Vorgaben da sitzt, die unspezifisch sind? Was, wenn du mit dem Kunden nur schwer reden kannst?

Dann sei der "Developer from Hell" für deinen Vorgesetzten :-) Baue ein bisschen und zeige es deinem Vorgesetzten. Nerve ihn mit Fragen wie "Ist das so richtig?" oder "Hat der Kunde sich das vielleicht so vorgestellt?" Immer und immer und immer wieder. Alle 2-3 Tage beim Vorgesetzten oder gar Kundenvertreter auf der Matte stehen und Feedback erbitten.

Dafür musst du natürlich in Durchstichen denken. Frag nicht, "Ist da so richtig mit dem Datenzugriff?" ;-) Das verstehen die nicht. Aber frag, "Sitzt der Button hier an der richtigen Stelle?" oder "Ist es korrekt, wenn der Umbruch des Textes das Wort so trennt?"

Wo der Kunde aus der Hölle kommt, da muss der Entwickler auch zur Hölle werden :-) Wie die Lateiner schon sagten: Abyssus abyssum invocat. Der Abgrund ruft den Abgrund.

-Ralf

Rüdiger Plantiko hat gesagt…

Hallo Ralf,

eine kleine Anmerkung zur "falsch verstandenen Objektorientierung":

wenn der arme Neal Ford Zahlen zu Objekten macht, ist das falsch verstandene Objektorientierung. Wenn aber Guillaume Laforge in Groovy eine DSL für einheitenbehaftete Zahlen entwirft, so dass er schreiben kann

3.cm + 12.m * 3 - 1.km

1.5.h + 33.s - 12.min

usw.

dann ist das wahnsinnig cool, und alle Clean Coder klatschen Beifall! :-)

Wird hier mit zweierlei Mass gemessen? Gibt es etwa coole Entwickler, die Zahlen als Objekte benutzen dürfen, und uncoole, die für dasselbe in Blogs schlechte Noten bekommen?

Es geht wohl auch hier nur darum, ob der resultierende Code lesbar ist. Wenn die Instanziierung des Zahl-Objekts wie in Groovy implizit ausgeführt wird und damit auch im Quelltext verborgen bleibt, dann ist das sehr im Sinne von "Clean Code". Hässlicher Code entsteht aber, wenn der Verwender der Zahlen die Instanzen zuerst umständlich beschaffen muss.

Gruss,
Rüdiger

Ralf Westphal - One Man Think Tank hat gesagt…

@Rüdiger: Ich habe nichts dagegen, wenn Neal Ford Zahlen zu Objekten macht. Die "Object Calisthenics" (http://www.xpteam.com/jeff/writings/objectcalisthenics.rtf) find ich ne interessante Übung. Und ich finde natürlich die F# Einheiten klasse.

In meinem Posting habe ich keinen Bezug zu solcherlei Objektorientierung drin. Schau nochmal rein. Mir ging es um viel Einfacheres.

Du sagst, es ginge um Lesbarkeit. Da bin ich ganz bei dir. Schau dir deshalb den im Blogposting in Frage stehenden Code von Neal nochmal an. Ich habe die Punkte herausgearbeitet, die ich für schlecht Objektorientiert halte, eben weil (!) sie den Code weniger verständlich machen. Zustand macht Code eben nicht per se besser lesbar. Das (!) ist meine Kritik. Und Verteilung von Logik auch nicht.

-Ralf

Rüdiger Plantiko hat gesagt…

Hallo Ralf,

Du schriebst aber: "Warum also sollte ich als Nutzer einer solchen Funktion(alität) eine Klasse instanzieren müssen? Warum sollte diese Klasse auch noch zustandsbehaftet sein?"

Die erste Frage stelle ich mir beispielsweise auch, wenn ich die Nachricht "sqrt" an das zuvor instanziierte Objekt "2" senden muss, um die Wurzel zu berechnen. Auch hier finde ich den Standpunkt natürlicher, dass es bestimmte objektunabhängige Funktionen gibt, die ich auf Zahlen anwenden kann. M.a.W. finde ich das Java-Konzept primitiver Datentypen, die selbst keine Objekte sind, gut.

Aber die erste Vorgehensweise wäre für mich auch OK, wenn die Instanziierung der "2" wie im Groovy-Beispiel vollständig hinter der Bühne abläuft, die Notation im Quelltext des Verwenders also klar und lesbar bleibt. Und darauf kommt es in der Hauptsache an.

Mit Deiner zweiten Frage, wieso Neal Ford für seine Funktion Zustand einführt, stimme ich dann wieder vollkommen überein. Das war aber, wie ich es gelesen habe, nur ein Teil Deiner Kritik.

Den Link auf die "objectcalisthenics" schaue ich mir gerne mal an, danke.

Gruss,
Rüdiger

Kommentar veröffentlichen

Hinweis: Nur ein Mitglied dieses Blogs kann Kommentare posten.