post

Warum Katas mehr sind als Fingerübungen

Eine Kata ist ein Architektur-Labor im Kleinen – weil die Aufgabe konstant bleibt, wird sichtbar, was sonst nur der Entwurf weiß.

≈ 8 Min. Lesezeit

Diesen Beitrag anhören (11 Min.)

MP3 herunterladen

Eine Kata ist in der Kampfkunst eine festgelegte Abfolge von Bewegungen, die man so oft wiederholt, bis sie ohne Nachdenken abläuft. Dave Thomas hat den Begriff vor gut zehn Jahren in die Programmierung geholt: kleine, scharf umrissene Aufgaben, die man nicht einmal löst, sondern immer wieder – FizzBuzz, der Bowling-Score, Roy Osheroves String Calculator. Auf codekata.com steht eine ganze Sammlung davon.

Der Vergleich mit der Kampfkunst trifft einen Punkt sehr genau und geht an einem anderen deutlich vorbei. Er trifft: Eine Kata findet ohne Ernstfall statt. Kein Kunde wartet, kein Termin drückt, kein Altsystem redet dazwischen. Man kann sich eine einzige Sache vornehmen und ihr die volle Aufmerksamkeit geben. Er hinkt: In der Kampfkunst ist das Ziel der Wiederholung, dass die Bewegung immer gleicher wird. Beim Programmieren wäre das sinnlos. Tippen ist nicht der Engpass, und wer den Bowling-Score zum fünften Mal identisch herunterschreibt, hat beim fünften Mal nichts gelernt.

Die Wiederholung lohnt sich aus einem anderen Grund. Weil die Aufgabe konstant bleibt, kann der Entwurf variieren – und genau dadurch werden Entwurfsentscheidungen sichtbar, die im Projektalltag unsichtbar bleiben. Eine Kata ist kein Tipp-Training. Sie ist ein Architektur-Labor im Kleinen.

Ein Labor braucht eine Konstante

Wer zwei Entwürfe ernsthaft vergleichen will, muss alles andere festhalten. Das ist im Projekt praktisch nie möglich. Dort ändert sich zwischen zwei Lösungsansätzen immer auch das Problem: neue Anforderungen, andere Kollegen, verschobene Prioritäten. Wenn die zweite Lösung besser aussieht als die erste, weiß niemand, ob der Entwurf besser war oder nur die Umstände günstiger. Aus einem Projekt lässt sich deshalb selten sauber ablesen, was eine einzelne Entwurfsentscheidung wirklich gekostet oder gebracht hat.

Die Kata dreht das um. Das Problem ist klein, vollständig bekannt und bleibt exakt gleich. Die Anforderungen stehen fest, bevor die erste Zeile entsteht. Was zwischen zwei Durchläufen variiert, ist ausschließlich der Entwurf: Wo liegt der Zustand? Was ist eine Funktion, was ein Objekt? Welche Grenze trennt Logik von Ein- und Ausgabe? Wenn sich Durchlauf drei leichter testen lässt als Durchlauf eins, dann liegt das nicht an der Aufgabe. Es liegt an einer Entscheidung, die ich getroffen habe – und die ich jetzt benennen kann.

Das klingt banal, ist es aber nicht. Mir ist erst über solche Wiederholungen aufgefallen, wie viele meiner Entwurfsgewohnheiten schlicht Gewohnheiten waren: Dinge, die ich tat, weil ich sie immer so tat, nicht weil sie der Aufgabe dienten. Im Projekt fällt so etwas nicht auf, weil nie jemand dieselbe Aufgabe ein zweites Mal anders löst. In der Kata fällt es sofort auf.

Zwei Regale voller Versuche

Auf meinem GitHub-Konto liegen zwei solche Sammlungen aus den letzten Jahren. Das ältere Repository heißt schlicht Katas, ist von 2010 und enthält C#-Übungen: eine Tennis-Kata und eine FizzBuzz-Variante, die ich damals mit den Reactive Extensions gebaut habe – nicht weil FizzBuzz das braucht, sondern weil ich wissen wollte, wie sich dieselbe triviale Aufgabe anfühlt, wenn man sie als Strom von Ereignissen statt als Schleife denkt. Ein README gibt es nicht, das Repository ist ein persönliches Übungsarchiv und will auch nichts anderes sein.

Das jüngere heißt AppKatas, ist von 2013 und in JavaScript geschrieben. Dort liegen unter anderem eine Calculator- und eine TicTacToe-Kata, beide bewusst als pures JavaScript mit Event-Sourcing umgesetzt, die Module laufen im Browser und unter Node.js, die Tests laufen über Travis. TicTacToe gibt es dort in zwei Ausbaustufen: als reine Browser-Anwendung und als Client-Server-Variante mit einem kleinen Node-Server.

Der Wert dieser Repositories liegt nicht im Code. Den würde ich heute an vielen Stellen anders schreiben. Der Wert liegt darin, dass dieselben Aufgaben mehrfach und unterschiedlich gelöst dort nebeneinanderstehen. Man kann die Entwürfe vergleichen, weil die Aufgabe konstant geblieben ist. Genau das meine ich mit Labor.

Testbarkeit ist Entwurfsdruck

Die Tennis-Kata sieht harmlos aus: Zähle die Punkte eines Tennisspiels und gib den Spielstand aus – Love, Fifteen, Thirty, Forty, Deuce, Advantage. Beim ersten Durchlauf 2010 habe ich das als eine Klasse mit zwei Zählern und einer großen Methode gebaut, die den Spielstand als Text zusammensetzt. Der erste NUnit-Test dazu sah ungefähr so aus:

[TestFixture]
public class TennisGameTests
{
    [Test]
    public void Score_AfterDeuce_ReturnsAdvantage()
    {
        var game = new TennisGame();
        for (var i = 0; i < 3; i++)
        {
            game.WinPoint(Player.One);
            game.WinPoint(Player.Two);
        }
        game.WinPoint(Player.One);

        Assert.AreEqual("Advantage Player One", game.Score());
    }
}

Der Test funktioniert, aber er verrät etwas: Um einen einzigen Spielstand zu prüfen, muss ich sieben Punkte in der richtigen Reihenfolge einspielen. Der Zustand ist im Objekt eingesperrt, und der einzige Weg hinein führt über die Historie. Bei Deuce ist das noch erträglich. Bei einer Aufgabe mit mehr Zustand wird genau dieses Muster zur Qual – jeder Test beginnt mit einem langen Aufbau, der mit der eigentlichen Behauptung nichts zu tun hat.

Das ist der Moment, in dem Testbarkeit zu Entwurfsdruck wird. Der schwer zu schreibende Test ist kein Test-Problem, er ist ein Symptom. Er zeigt, dass der Entwurf keinen direkten Zugang zu dem Zustand erlaubt, um den es fachlich geht. Im zweiten Durchlauf habe ich den Spielstand als Wert modelliert, den man direkt konstruieren kann, und die Übergangslogik als Funktion von Zustand zu Zustand. Die Tests schrumpften auf drei Zeilen, und nebenbei war die große Textmethode in kleine, benennbare Fälle zerfallen.

Im Projekt hätte ich diesen zweiten Durchlauf nie gemacht. Die erste Version funktionierte ja. Niemand baut ein funktionierendes Stück Software noch einmal, nur um zu sehen, ob der Entwurf auch anders ginge. In der Kata ist genau das der Zweck der Veranstaltung.

Dieselbe Aufgabe, zwei Entwürfe

Am deutlichsten habe ich den Labor-Effekt bei TicTacToe in AppKatas gesehen. Die naheliegende erste Version in JavaScript sieht so aus, wie vermutlich die meisten sie schreiben würden – ein Spielobjekt, das ein Brett hält und es bei jedem Zug verändert:

function Game() {
  this.board = [null, null, null, null, null, null, null, null, null];
  this.currentPlayer = "X";
}

Game.prototype.move = function (field) {
  if (this.board[field] !== null) {
    throw new Error("field already taken");
  }
  this.board[field] = this.currentPlayer;
  this.currentPlayer = this.currentPlayer === "X" ? "O" : "X";
};

Das ist völlig in Ordnung und trägt ein kleines Spiel problemlos. Interessant wird es beim zweiten Durchlauf mit einer anderen Grundentscheidung: Das Spiel speichert kein Brett, sondern nur die Liste der Züge. Das Brett wird bei Bedarf aus den Ereignissen abgeleitet:

function replay(events) {
  var board = [null, null, null, null, null, null, null, null, null];
  events.forEach(function (event) {
    board[event.field] = event.player;
  });
  return board;
}

function move(events, field) {
  var board = replay(events);
  if (board[field] !== null) {
    throw new Error("field already taken");
  }
  var player = events.length % 2 === 0 ? "X" : "O";
  return events.concat([{ player: player, field: field }]);
}

Kein this, keine Mutation, kein Konstruktor. move nimmt eine Liste und gibt eine neue Liste zurück. Die Konsequenzen dieser einen Entscheidung fallen einem regelrecht in den Schoß: Ein Test ist jetzt eine Liste rein, eine Liste raus – jeder Spielstand lässt sich direkt hinschreiben, ohne Züge einspielen zu müssen. Rückgängig machen ist das Weglassen des letzten Elements. Und die Client-Server-Variante wurde beinahe trivial, weil eine Zugliste sich als JSON verschicken lässt, während ein Objekt mit Methoden und innerem Zustand an der Prozessgrenze auseinanderfällt.

flowchart TD
  A[Gleiche Aufgabe:<br/>TicTacToe] --> B[Entwurf 1:<br/>Zustand wird mutiert]
  A --> C[Entwurf 2:<br/>Zugliste, Brett abgeleitet]
  B --> D[Test braucht<br/>Zug-Historie als Aufbau]
  C --> E[Test ist<br/>Liste rein, Liste raus]
  D --> F[Unterschied ist dem Entwurf<br/>zurechenbar, weil das<br/>Problem konstant blieb]
  E --> F

Der Punkt ist nicht, dass Ereignislisten besser wären als veränderlicher Zustand. Für ein TicTacToe im Browser ist die erste Version schlicht angemessen, und replay bei jedem Zug komplett laufen zu lassen, skaliert erkennbar nicht für große Ereignismengen. Der Punkt ist, dass ich beide Entwürfe an derselben Aufgabe nebeneinander halten kann. Erst dadurch sehe ich, was die Entscheidung kostet und was sie kauft. Im Projekt bekomme ich immer nur einen der beiden Zweige zu sehen – den, für den wir uns entschieden haben. Der andere bleibt Spekulation.

Nebenbei zwingt die zweite Version zu einer sauberen Grenze, die in der ersten gern verwischt: Die Spiellogik weiß nichts vom DOM. replay und move laufen unverändert unter Node und im Browser, die Darstellung ist ein dünner Rand, der Ereignisse entgegennimmt und ein Brett zeichnet. Diese Grenze habe ich nicht gezogen, weil ein Prinzip es verlangt, sondern weil der Entwurf sie mir aufgedrängt hat. Solche aufgedrängten Grenzen merkt man sich besser als gepredigte.

Der Luxus des Wegwerfens

Zur Kata gehört ein Schritt, der im Projekt undenkbar ist: Am Ende wird der Code gelöscht. Nicht archiviert, nicht als Basis für den nächsten Durchlauf behalten – gelöscht. Beim nächsten Mal beginnt man leer.

Anfangs kam mir das verschwenderisch vor. Inzwischen halte ich es für den wichtigsten Teil. Solange Code weiterlebt, verteidigt man ihn. Man baut auf ihm auf, umgeht seine Schwächen, gewöhnt sich an seine Form. Erst wenn feststeht, dass er verschwindet, wird man ehrlich zu ihm. Dann probiert man die Entscheidung aus, die man im Projekt nie riskieren würde, weil dort jede Entscheidung sofort Bestandsschutz bekommt.

Genau darin liegt der Unterschied zwischen Üben und Arbeiten. Im Projekt ist Code ein Investitionsgut, und jede Alternative konkurriert mit dem, was schon da ist. In der Kata ist Code Versuchsmaterial. Man darf eine Idee zu Ende denken, feststellen, dass sie nicht trägt, und sie ohne Kosten begraben. Die Erkenntnis überlebt, der Code nicht. Das ist die richtige Reihenfolge.

Was eine Runde für mich ausmacht

Mit der Zeit hat sich bei mir eine einfache Form eingespielt, ganz ohne Rahmenprogramm – eine Stunde, ein Editor, eine Aufgabe:

  • Eine bekannte Aufgabe wählen, keine neue. Die Neugier soll dem Entwurf gelten, nicht dem Problem.
  • Vor dem ersten Test eine einzige Entwurfsentscheidung festlegen, die diesmal anders ausfällt als beim letzten Mal.
  • Tests zuerst schreiben und jeden zähen Test als Hinweis auf den Entwurf lesen, nicht als Test-Problem.
  • Am Ende zwei, drei Sätze notieren: Was war leichter, was schwerer, und woran lag es?
  • Den Code löschen.

Die Notizen sind unscheinbar, aber sie sind das eigentliche Ergebnis. Nach ein paar Runden mit derselben Aufgabe steht dort eine kleine, selbst erarbeitete Karteikartensammlung über Entwurfsentscheidungen – belegt an Code, den man selbst geschrieben hat, statt geglaubt aus einem Buch.

Was hängen bleibt

Fingerübung ist das falsche Wort, weil es das Ziel falsch beschreibt. Geübt wird nicht die Hand, geübt wird das Urteil. Eine Kata hält das Problem fest, damit der Entwurf variieren kann, und macht dadurch sichtbar, was eine Entwurfsentscheidung wirklich bewirkt: welche Tests sie leicht oder zäh macht, welche Grenzen sie erzwingt, was sie an einer Prozessgrenze kostet. Das sind genau die Fragen, die in großen Systemen über Jahre nachwirken – nur dass man sie dort nie isoliert beobachten kann.

Deshalb halte ich die halbe Stunde mit FizzBuzz, Tennis oder TicTacToe nicht für vertane Zeit, obwohl dabei nichts entsteht, was jemand benutzt. Es entsteht etwas anderes: die Erfahrung, dieselbe Aufgabe mit zwei Entwürfen gesehen zu haben. Wer die einmal hat, argumentiert in der nächsten Architektur-Diskussion nicht mehr mit Vorlieben, sondern mit Beobachtungen. Das ist mehr, als eine Fingerübung je liefern könnte.

Weiterführende Quellen

Kommentare