zum Inhalt

Der lise Blog:
Einsichten, Ansichten, Aussichten

Lessons Learned aus der Gilded Rose Kata

18. Oktober 2018 von Avatar of SteveSteve

Schon vor Jahren habe ich die Gilded Rose Kata schätzen gelernt. Im Rahmen der lise Developer Meetings und bei der Softwerkskammer Köln haben wir vor kurzer Zeit die Gilded Rose Kata erneut bearbeitet. In diesem Blogartikel präsentiere ich einige Lessons Learned.

Vorneweg: Was ist eine Kata? Und, was ist die Gilded Rose Kata? Als Einführung zur Kata dienen meine Slides, die ihr auf speakerdeck abrufen könnt.Eine Kata ist eine Übungsaufgabe, um seine Fähigkeiten zu vertiefen und auszubauen. Dieser Begriff kommt ursprünglich aus den asiatischen Kampfkünsten und beschreibt eine Übungsabfolge, die solo oder im Paar geübt wird. Die Gilded Rose Kata ist eine Übungsaufgabe speziell zum Thema Refactoring, in der es gilt, ein bestehendes System zu refaktorisieren und anschließend zu erweitern. Im Kern gilt es, eine 30-zeilige Methode voller If-Statements und vermischten Konzepten aufzulösen, ohne das externe Verhalten zu verändern. Vor dem Lesen dieses Blogpost ist es sinnvoll, die Aufgabenstellung der Kata durchzulesen und sie selbst durchzuarbeiten. 

Denn: Das Programmierhandwerk besteht aus der Kunst, nicht aufzuhören, wenn Aufgaben fertig sind, sondern dann den Code noch „schön” zu machen. Was heißt schön? Martin Fowler hat es auf den Punkt gebracht: „Any fool can write code, that a computer understands. Good programmers write code, that humans understand.”

1) Sicherheitsnetz spannen

Bevor wir anfangen, Code zu refaktorisieren, ist es notwendig, sicherzustellen, dass wir eine Rückmeldung bekommen, falls wir den Code in seinem äußeren Verhalten ändern. Refactoring möchte genau das nicht, es geht nur um das Aufräumen und Umstrukturieren, wir wollen explizit keine neue Funktionalität einbringen oder bisherige Funktionalität anpassen. Wie kann ich mir sicher sein, dass ich nichts kaputt mache? Ich brauche ein Sicherheitsnetz aus automatisierten Tests, die ich nach jedem Schritt ausführen kann und die mir bestätigen, dass alles weiterhin, wie erwartet, läuft. 

Ein Weg ist Approval Testing: Dabei erzeuge ich vom bisher unberührten Produktiv-Code eine Textausgabe und sichere mir diese in eine Datei. Anschließend schreibe ich einen Test, der die gesicherte Textausgabe mit einer erneut erzeugten Textausgabe vergleicht. Dies bezeichnet man als Text-Based-Testing beschrieben. Wenn ich versehentlich das Verhalten ändere, ändert sich die produzierte Textausgabe und ich habe einen Hinweis durch den fehlgeschlagenen Test. Durch Approval Testing erkenne ich, dass etwas kaputt gegangen ist, aber noch lange nicht warum. Insofern sind Approval Tests nicht mit typischen Unit Tests vergleichbar, welche ein gewünschtes Verhalten beschreiben und aufzeigen, welchem gewünschten Verhalten nicht mehr entsprochen wird. 

Ein Problem an Unit Tests ist die enge Verzahnung mit dem Produktiv-Code. Anhand der Gilded Rose lässt sich aufzeigen, wie man diese Verzahnung einschränkt. Die Unit Tests instanziieren die Gilded Rose und lassen ein Item in einem bestimmten Zustand aktualisieren und überprüfen. Dabei weiß der Unit Test nicht, wie im Hintergrund die Gilded Rose das Update durchführt. Liegt eine Methode vor (Ausgangsbasis) oder liegen Aufrufe zu Methoden anderer Klassen dahinter? Es ist dem Test egal, Hauptsache die öffentliche Schnittstelle bleibt erhalten. Man schafft es also, die darunter liegende Struktur mit der Gilded Rose als „Unit” zu betrachten und dies als unterste Ebene für Tests anzunehmen. Es besteht kein Bedarf, jede einzelne Klasse als „Unit” zu verstehen und deswegen Tests zu schreiben. Ändert sich im Hintergrund etwas an der Struktur, zum Beispiel durch unser Refactoring, erfordert das keine Änderung an unserem Test-Code.

Ganz gleich, wie wir uns das Sicherheitsnetz spannen: Wie stellen wir sicher, dass dieses Netz ausreichend groß ist? Metriken wie Code oder Branch Coverage könnten eine falsche Sicherheit vermitteln. Ergänzen lässt sich das durch Mutation Testing, welches den Produktiv-Code mutiert und überprüft, ob durch die Änderung ein Test fehlgeschlagen ist und damit die Mutation aufdeckt. 

In unserem Developer Meeting haben sich drei von zehn Paaren entschieden, neben den vorliegenden Approval Tests Unit Tests zu schreiben. Am Ende des Developer Meetings hatten vier Paare die Aufgabe abgeschlossen, sechs nicht. Von den vier Paaren waren zwei dabei, die Tests geschrieben haben. Dem Argument „Testen kostet zu viel Zeit“ kann man also getrost den Wind aus den Segeln nehmen. Natürlich ist es eine Umstellung, testbaren Code zu schreiben und es benötigt Routine im Formulieren von Tests.

Ein weiterer Vorteil einer guten Testabdeckung ist, dass Abweichungen zwischen der Spezifikation und dem tatsächlichen Verhalten aufgedeckt werden können. Die Beschreibung der Gilded Rose formuliert alle Verhaltensweisen. Aber verhält sich die Gilded Rose tatsächlich wie beschrieben? Die Wahrheit liegt letztlich im Code.

2) Ziel des Refactorings bewusst machen

Bisher haben wir eine unschöne, um nicht zu sagen, hässliche Ausgangslage und den Ansporn, dies zu beheben. Aber wie sieht unser Ziel aus? Orientierungshilfe bieten mehrere Punkte:

Zum einen die SOLID-Prinzipien, anhand deren man Verstöße feststellt. Beispielsweise verstößt die Gilded Rose gegen das Single Responsibility Prinzip. Sie enthält die Geschäftslogik für sämtliche Typen von Items. Das ist definitiv zu viel. Zum anderen ist die Lesbarkeit des Codes durch die vielen Verschachtelungen und Duplizierungen eingeschränkt. Hier helfen die "4 Rules of Simple Design" als Orientierungspunkt. Der Code soll ausdrücken, was seine Intention ist. Man sollte die Frage „Was möchte ein Stück Code erreichen” nicht implizit durch das „Wie erreicht der Code sein Ziel” ausdrücken. Der einfachste Einstieg ist, kleine Methoden aus den unzähligen Code-Blöcken zu schaffen, bei denen der Methoden-Name beschreibt, was passieren soll. Der Inhalt der Methode zeigt dann die konkrete Implementierung. Für den High-Level-Überblick reicht es, im Code durch die klar verständlichen Methoden-Namen nachzuvollziehen, was der Code erreichen möchte. Benötige ich Detailwissen, wie das erreicht wird, schaue ich mir den Inhalt der Methode an.

Konkrete Ziele können neben den beschriebenen Orientierungspunkten konkrete Design Patterns sein, wie sie von der Gang of Four aufgezeigt werden. Darüber hinaus gibt es noch weitere, wie die Möglichkeit von Vererbung und Polymorphie in einer objektorientierten Sprache wie C#. Auch weitere Sprach-Features können in bestimmten Situationen hilfreich sein. Zum Beispiel Extension Methods aus C# helfen, Klassen um Methoden anzureichern, die man selber aber nicht verändern darf. Die Optionen und Lösungen sind vielfältig.

Wichtig ist bei den Lösungsmöglichkeiten, als Team zu bestimmen, welchen Stil man anwendet, sodass der Code, ganz gleich, von wem er geschrieben wurde, für jeden anderen nachvollziehbar ist. Dieses gemeinsame Verständnis im Team kann man sehr gut anhand der Kata aufbauen! 

Die häufigste Lösung der Kata beinhaltet die Kombination aus den Patterns Strategy und Factory. Je Item-Typ wird eine eigene Strategy formuliert, die Factory bestimmt anhand des Typs, welche Strategy für das Update auf das Item angewendet werden soll. Auch die Anwendung von Vererbung, dem Template Method Pattern oder auch das Rule Pattern wurden in unseren Sessions angewendet.

3) Der Weg zum Ziel

Jetzt haben wir uns einerseits ein Sicherheitsnetz gespannt und andererseits Ideen entwickelt, wohin wir mit dem Code möchten. Aber wie kommen wir dahin? Es gibt verschiedene Refactoring-Strategien. Davon stelle ich ein paar vor: 

Wenn wir uns den Code anschauen, sollte auf jeden Fall einleuchten, dass wir den Code erst aufräumen, bevor wir versuchen, ein neues Feature in diese Methode einzubauen. Diese Strategie nennt sich „Nesting” und leitet sich aus der Natur ab. Bevor ein Vogel Nachwuchs bekommt, baut er ein Nest, in dem das Ei gebrütet werden kann. Genau das möchten wir auch. Wir möchten zunächst den Code vorbereiten, sodass wir die Änderung am Code machen können. Dazu gibt es ein passendes Zitat von Kent Beck: „First make the change easy, (warning: this might be hard!) then make the easy change.”

Ein Weg dies zu tun, beschreibt die Strategie „Parallel Change”. Stellt euch vor, eine Brücke ist marode und sie wird neu gebaut. Wir bauen diese neue Brücke parallel zur Bestehenden und lassen den Verkehr solange über die alte Brücke fahren bis wir mit der neuen Brücke fertig sind. Dann leiten wir den Verkehr um und reißen die alte Brücke ab. Auf den Code übertragen bedeutet das: Wir schaffen eine neue Struktur und hangeln uns anhand der Tests entlang, ohne die Logik der bisherigen Methode nachvollziehen zu müssen.

Ein anderer Weg wäre, ständig ganz kleine Änderungen vorzunehmen und anschließend sicherzustellen, dass die Tests weiterhin laufen. Diese Strategie lautet „Piecemeal Refactoring”. Wir essen also jede Erbse einzeln und vergewissern uns, ob wir noch hungrig sind. Je kleiner die Schritte sind, die wir machen, desto einfacher ist es, den letzten Schritt rückgängig zu machen. Maximiere die Zeit „unter grün” (erfolgreicher Tests), minimiere die Zeit „unter rot” (fehlschlagender Tests)! Aber das gilt unabhängig der gewählten Strategie: Make Baby Steps!

Noch ein Tipp: Sandi Metz hat einen Talk auf Basis der Gilded Rose Kata gehalten. In diesem stellt sie ihr Vorgehen vor und zeigt anhand von Metriken, wie sich die Qualität des Codes verbessert hat.

Foto: https://www.flickr.com/photos/th_hansen/11151520553

Newsletter-Anmeldung

Newsletter-Anmeldung
* Dieses Formular speichert Ihre E-Mail-Adresse. Weitere Informationen in unseren Datenschutzbestimmungen.