Die Coding-Dojo-Welt hat sich geteilt. Es gibt nun den münchener Latifa-Stil von Ilker. Und es gibt den Schwarm-Stil von Stefan Lieser und mir. Bei der Teilung hat es ein wenig gerumpelt in der Community – aber nun sollten wir in friedlicher Koexistenz coden können. Die Welt ist halt bunt.
Den einen wahren, kanonischen Coding-Dojo-Stil gibt es nicht. Es gab und gibt nur Interpretationen dessen, was Leute wie z.B. Robert C. Martin mal gezeigt haben. Einzig in der Praktik des TDD sind sich die Dojo einig – und natürlich darin, dass Code geschrieben werden soll.
Über den Prozess, wie zu diesem Code im Dojo gekommen werden sollte, ist schon geschrieben worden. Das ist es, worin sich Latifa- und Schwarm-Stil unterscheiden. Und das macht natürlich einen Unterschied im Lernen der Teilnehmer.
Aber welchen Einfluss hat das auf das Produkt eines Dojos? Merkt man es dem Code an, ob er nach diesem oder jenem Stil entwickelt wurde? Ich habe mal versucht, das herauszufinden.
Für die Kata FizzBuzz habe ich versucht, mich in den Latifa-Stil hinein zu versetzen. Ich hoffe, das Ergebnis ist nicht zu sehr gefärbt von irgendwelchen Missverständnissen oder Voreingenommenheiten.
Anschließend habe ich den Code nach Schwarm-Stil Manier nochmal entwickelt.
FizzBuzz à la Latifa
Gerade nach der Erfahrung des letzten Coding Dojo zum dotnetpro.powerday bin ich mir sicher, dass ein Latifa-Dojo die grundsätzliche Codestruktur so aufgesetzt hätte:
Implementation und Test sind in einem Projekt zusammengefasst. Begründung: Mehr braucht es nicht. Das Beispiel ist so klein, dass alles andere Overkill wäre.
Von der Implementation nehme ich an, dass sie im Wesentlichen so aussehen würde:
public class FizzBuzzer
{
public List<string> Generate()
{
var numbers = new List<string>();
for (int i = 1; i <= 100; i++)
{
if (IsFizz(i) && IsBuzz(i))
numbers.Add("fizzbuzz");
else if (IsFizz(i))
numbers.Add("fizz");
else if (IsBuzz(i))
numbers.Add("buzz");
else
numbers.Add(i.ToString());
}
return numbers;
}private bool IsBuzz(int i)
{
return i % 5 == 0;
}private bool IsFizz(int i)
{
return i % 3 == 0;
}
}
Die Aufgabe ist klein. Hexenwerk ist nicht nötig. Alles geradlinig implementiert. Die Funktionen IsFizz() und IsBuzz() sind bei einer Refaktorisierung herausgezogen worden.
Exemplarisch hier auch noch ein Ausschnitt aus den Tests:
[TestFixture]
public class Test_FizzBuzzer
{
[Test]
public void Check_numbers()
{
var sut = new FizzBuzzer();var numbers = sut.Generate();
Assert.AreEqual("1", numbers[0]);
Assert.AreEqual("91", numbers[90]);
}[Test]
public void Check_fizz()
{
var sut = new FizzBuzzer();var numbers = sut.Generate();
Assert.AreEqual("fizz", numbers[2]);
Assert.AreEqual("fizz", numbers[98]);
}
…
Sie setzen alle beim FizzBuzz-API an – öffentliche Methode Generate() der FizzBuzzer-Klasse. Alle Tests sind Black Box Tests.
So, wie ich bisher die Latifa-Dojos erfahren habe, glaube ich nicht, dass weitere Refaktorisierungen vorgenommen worden wären. Ich glaube auch nicht, dass andere Datenstrukturen gewählt worden wären.
Und warum auch? Läuft doch.
FizzBuzz à la Schwarm
Nach solcher (visionierter) Vorlage eines Latifa-Dojos stellt sich die Frage, inwiefern eine Lösung überhaupt anders aussehen kann. Das Problem ist ja trivial. Kann es da überhaupt zwei Meinungen geben?
Ja, es gibt Alternativen. Der Schwarm-Stil unterscheidet sich also nicht nur im Prozess vom Latifa-Stil, sondern auch in seinem Anspruch an die Lösungsformulierung. Die beginnt bei der Codeorganisation:
Zwei Projekte, statt einem. Denn beim Schwarm legen wir Wert nicht nur auf TDD und Funktionalität, sondern auch auf Klarheit in der Form. Verständlichkeit ist uns wichtig. Separation of Concerns (SoC) gilt nicht nur innerhalb von Code, sondern auch für seine Form. Deshalb sind für uns Tests und Implementation immer getrennt. Der Schwarm bemüht sich, möglichst viele Clean Code Developer Bausteine zu berücksichtigen.
An der groben Codeorganisation ist aber noch mehr als eine SoC abzulesen. Das Testprojekt besteht aus drei Klassen. Die spiegeln wider, dass vor dem Coden über den Lösungsansatz nachgedacht wurde. Es hat eine kurze Kreativphase gegeben, in der erkannt wurde, dass die Lösung ein Prozess ist, der aus zwei Phasen besteht:
- Zahlengenerierung
- Zahlenübersetzung (Mapping).
Um diese Erkenntnis im Code festzuhalten, habe von vornherein für beide Phasen Methoden im Code vorgesehen. Und deshalb konnte ich mich auch entscheiden, welche ich davon zuerst nach TDD implementiere. Ich habe mich für die Zahlenübersetzung entschieden. Sie ist ja auch der Kern des FizzBuzz-Problems.
public class FizzBuzzer
{
internal static string MapNumber(int number)
{
if (IsFizz(number) && IsBuzz(number)) return "fizzbuzz";
if (IsFizz(number)) return "fizz";
if (IsBuzz(number)) return "buzz";return number.ToString();
}private static bool IsBuzz(int number)
{
return number % 5 == 0;
}private static bool IsFizz(int number)
{
return number % 3 == 0;
}
Natürlich habe ich die Tests jeweils vorher geschrieben:
[TestFixture]
public class Test_MapNumber
{
[Test]
public void Map_straight_number()
{
Assert.AreEqual("1", FizzBuzzer.MapNumber(1));
Assert.AreEqual("2", FizzBuzzer.MapNumber(2));
Assert.AreEqual("4", FizzBuzzer.MapNumber(4));
Assert.AreEqual("98", FizzBuzzer.MapNumber(98));
}[Test]
public void Map_fizz_number()
{
Assert.AreEqual("fizz", FizzBuzzer.MapNumber(3));
Assert.AreEqual("fizz", FizzBuzzer.MapNumber(6));
Assert.AreEqual("fizz", FizzBuzzer.MapNumber(99));
}
Von den Testfällen her unterscheiden die sich eigentlich nicht vom Latifa-Stil. Aber betonenswert anders ist, dass ich sofort mit dem Kern des Problems anfangen konnte, weil ich wusste, dass es dafür eine Funktionseinheit gibt. Und die Tests sind fokussierter, weil sie nur die Übersetzung testen und nicht immer auch noch die Zahlengenerierung berücksichtigen müssen. Schließlich ist der Code in der MapNumber()-Methode auch noch simpler als der Übersetzungscode beim Latifa-Stil, weil er weniger tief schachtelt (keine else-Zweige).
Hätte der Latifa-Stil mit konsequentem TDD auch dahin kommen können? Klar. Aber die Latifa-Dojo-Erfahrung zeigt, dass das nicht passiert. Da es im Latifa-Stil kein “offizielles” Nachdenken über den Lösungsansatz gibt, gibt es auch keine Gewissheit, dass das demokratische Voranschreiten so tief ins Refactoring einsteigt, wo es doch viel spannender ist, überhaupt eine lauffähige Lösung zu produzieren.
Nach Implementation des Mappings habe ich die Zahlenerzeugung implementiert. Die ist natürlich trivial. Dem Schwarm-Stil ist das jedoch egal. Wenn in der Kreativphase ein Lösungsansatz erarbeitet wurde, der die Zahlengenerierung ausweist, dann wird sie in einer expliziten Funktionseinheit implementiert. Das entspricht dem Single Responsibility Principle (SRP).
internal static IEnumerable<int> NumberGenerator(int from, int to)
{
for (var i = from; i <= to; i++)
yield return i;
}
Die Zahlenerzeugung ist kein Bestandteil des API. Deshalb erfolgt sie in einer internen Methode.
Beim Testen ist zu sehen, dass auch der Schwarm-Stil pragmatisch vorgeht. Ich habe nur einen Test geschrieben:
[TestFixture]
public class Test_NumberGenerator
{
[Test]
public void Numbers_1_to_3()
{
var numbers = FizzBuzzer.NumberGenerator(1, 3);
Assert.AreEqual(new[]{1,2,3}, numbers.ToArray());
}
}
Im Sinne eines universellen Zahlengenerators ist das natürlich zuwenig. Wie würde der z.B. auf Zahlengrenzen < 0 reagieren? Wie würde er reagieren, wenn die erste Zahl größer als die zweite ist? Eine Beschränkung auf den problemrelevanten Happy Day Fall ist auch für den Schwarm-Stil ok.
Die Schritte des Lösungsansatzes müssen am Ende natürlich noch zusammengezogen werden. Das ist die Aufgabe der API-Methode:
public IEnumerable<string> Generate()
{
return NumberGenerator(1, 100).Select(MapNumber);
}
Im Vergleich zur Latifa-Lösung folgt sie sofort dem Single Level of Abstraction (SLA) Prinzip. Die Lösung wird auf hohem, einheitlichem Abstraktionsniveau beschrieben. Und die Phasenfolge ist klar zu erkennen. Schwer zu verstehende Schachtelung ist nicht nötig.
Da die Einzelfunktionalitäten schon getestet sind, ist für die Integration nur noch ein Integrationstest nötig:
[TestFixture]
public class Integrationtest
{
[Test]
public void Check_number_generation_and_mapping()
{
var sut = new FizzBuzzer();
Assert.AreEqual(
new[] { "1", "2", "fizz" },
sut.Generate().ToArray().Take(15));
}
}
Der Test kann so kurz ausfallen, weil lediglich geprüft werden muss, ob die Funktionseinheiten überhaupt korrekt zusammenspielen.
Fazit
Wenn ich die typischen Vorgehensweisen von Latifa- und Schwarm-Stil hier halbwegs wirklichkeitstreu wiedergegeben habe, dann führen sie zu unterschiedlichem Code. Ob das eine Ergebnis besser oder schlechter ist, will ich nicht beurteilen. Mir war nur die Beantwortung der Frage wichtig, ob es überhaupt einen Unterschied gibt. Und wenn ja, was der ausdrückt.
Unterm Strich scheint mir hier Conway´s Law bestätigt: Der Code spiegelt Prozess und Organisation des Teams wider. Latifa setzt auf Demokratie, d.h. Gleichberechtigung und Gleichstellung, und hat darüber hinaus keinen Prozess jenseits TDD.
Schwarm setzt auf einen klaren Prozess (Kreativphase gefolgt von Umsetzungsphase) und die Einhaltung von Prinzipien jenseits von TDD bei der Umsetzung.
Nun mag jeder (potenzielle) Teilnehmer an einem Dojo entscheiden, welchen Stil er/sie bevorzugt.