Refactorings mal anders: planbar und strukturiert!

Michael Graf
Digital Frontiers — Das Blog
9 min readAug 6, 2021
Bild von Johnny Gutierrez auf Pixabay

Durch Refactorings ist es möglich, die internen Strukturen und Abläufe einer Software anzupassen. Sie sind immer dann nötig, wenn sich grundlegende Annahmen, wie zum Beispiel das Mengengerüst oder die nicht-funktionalen Anforderungen durch ein neues Feature ändern. In der klassischen Architektur entsprechen Sie etwa einer nachträglichen Anpassung der Statik oder des Fundaments. Größere Refactorings stellen daher Entwickler-Teams immer wieder vor Herausforderungen. Sie sind ohne strukturierte Analyse nur extrem schwer schätzbar. Die zu Beginn geplanten Aufwände müssen daher aufgrund von vorher nicht bedachten bzw. nicht entdeckten Problemen wiederholt nach oben korrigiert werden. Häufig werden in der Eile, um im Zeitplan zu bleiben, automatisierte Tests weggelassen und auch die manuellen Tests werden vernachlässigt. Hierdurch schleichen sich unweigerlich Fehler ein, welche erst im Betrieb auffallen. Zusätzlich wird der Aufwand bei Refactorings dadurch nach oben getrieben, dass es bei der parallelen Weiterentwicklung immer wieder zu Merge-Konflikten und Doppelentwicklungen kommt.

Einige Projektverantwortliche betrachten Refactorings daher als Kostenfalle und würden diese am liebsten ganz weglassen. Dabei ist es mittlerweile allgemein bekannt, dass eine Software nicht 30 Jahre ohne irgendeine Anpassung betrieben werden kann. Kundenanforderungen ändern sich, Frameworks und Anbindungen an Drittsysteme müssen beispielsweise aktualisiert werden. Um eine Software und ihre Architektur an diese neuen Gegebenheiten und Anforderungen anzupassen, sind Refactorings also zwingend notwendig. Werden sie nicht oder nur halbherzig durchgeführt, führt das zu technischen Schulden und sehr schnell zu den altbekannten Legacy-Projekten aus der Wasserfallära, mit denen alle Beteiligten unzufrieden sind.

Trotzdem fällt es immer wieder schwer, Refactoring-Aufwände gegenüber den Projektverantwortlichen zu rechtfertigen. Da die Aufwände schlecht oder nur mit einem „Bauchgefühl“ begründet werde können, enthalten die Schätzungen oft einen hohen Risikofaktor und variieren stark. Dies liegt aber nicht am Vorhaben, die Strukturen einer Software zu verbessern. Das Problem liegt vielmehr darin, dass Refactorings aufgrund unklarer Abhängigkeiten ohne vorherige Analyse schlecht abschätzbar und planbar sind.

ISBN-13 : 978–1617291210

Ola Ellnestam und Daniel Brolund haben mit der Mikado-Methode ein strukturiertes Vorgehen für Refactorings beschrieben. Die Mikado-Methode kann aber nicht nur zur Durchführung von Refactorings verwendet werden. Sie ist auch ideal geeignet, um Refactorings im Vorfeld zu analysieren und die entstehenden Aufwände besser einzuplanen.

Mikado-Methode im Detail

Die Mikado-Methode besteht aus Iterationen mit je vier Schritten:

Set Goal; Experiment; Visualize; Undo
  1. Im ersten Schritt definieren wir das zu erreichende Ziel des aktuellen Vorhabens.
  2. Im anschließenden zweiten Schritt „Experiment“ setzen wir dieses Ziel experimentell um. Wir wollen keine saubere, schöne oder gar funktionierende Umsetzung, sondern Abhängigkeiten und Probleme für das in Schritt 1 definierte Ziel finden und aufdecken.
  3. Die gefundenen Abhängigkeiten und Probleme werden nun im dritten Schritt „Visualize“ notiert und visuell festgehalten. Sie stellen somit die Vorbedingungen des Ziels aus Schritt 1 dar.
  4. Anschließend werden die Code-Änderungen des Experiments rückgängig gemacht, damit die Software sich wieder in einem sauberen, funktionsfähigen Zustand befindet. Hierdurch wird sichergestellt, dass wir im nächsten Experiment der nächsten Iteration alle Probleme und Abhängigkeiten erkennen können und wir keine Probleme aufgrund von Überlagerungen übersehen.
    Das Undo klingt hier sehr hart, was es aber nicht ist: Zum einen handelt es sich nur um eine schnelle prototypische Umsetzung, welche wir hier verwerfen. Zum anderen wurden die wichtigen Erkenntnisse und das erarbeitete Wissen bereits im vorherigen Schritt 3 festgehalten. Der Verlust ist somit minimal. Es handelt sich vielmehr um ein „Clean Up“ für die nächste Iteration.

Sind alle Änderungen rückgängig gemacht und der Ursprungszustand wieder hergestellt, wählen wir eine der entdeckten Abhängigkeiten als neues Ziel aus und beginnen mit dem nächsten Experiment. Nach kurzer Zeit entsteht hierdurch ein Baum von Abhängigkeiten für das Refactoring.

Ein Beispiel

Schauen wir uns das ganze an einem Beispiel an:

In einer fiktiven Anwendung wollen wir die Kunden-Logik mit der Hilfe einer Strategy-Klasse kapseln, welche anschließend über eine Factory ausgewählt werden soll. Ein Baum für ein solches Vorhaben könnte wie folgt aussehen:

um besseren Verständnis wurde hier recht grobe Schritte verwendet. Die einzelnen Schritte sind in der Praxis meistens deutlich feingranularer und technischer.

Das Ziel in diesem Beispiel ist es, die Kundenlogik per Factory-Klasse auszulagern. Als Ergebnis des ersten Experiments müssen wir zuerst die Konfiguration der Anwendung erweitern und die API für die verschiedenen Kunden-Implementierungen definieren. Beim Erstellen der API-Klasse stellen wir fest, dass zuerst die über mehrere Services verteilte Kundenlogik zusammengeführt werden muss. Beim Zusammenführen der Logik wiederum finden wir Business-Logik in der UI, für die es zudem keine Tests gibt. Bei einem klassischen Vorgehen hätten wir, spätestens nach dem Versuch der Zusammenführung der Kunden-Logik, Compiler-Fehlermeldungen und somit keine Möglichkeit mehr, unsere Tests sauber durchzuführen. Der Umbau der UI müsste somit im Blindflug stattfinden, was zu den bekannten und hohen Aufwänden bei Refactorings führt. Durch das Zurücksetzen der Codebasis nach jedem Experiment gibt es dieses Problem hier nicht. Es stehen uns immer alle Tests zur Verfügung und wir starten immer auf einer sauberen Codebasis.

Wird während eines Experiments keine Abhängigkeit gefunden, haben wir ein Blatt im Baum erreicht. Dieser Schritt des Refactorings hat somit keine Vorbedingungen. Ein solches Blatt können wir direkt umsetzen und anschließend im Baum abhaken. Sind alle Vorbedingungen eines Knotens erfüllt und abgehakt, können wir diesen ebenfalls wie ein Blatt abarbeiten. In diesem Beispiel sind das u.a. die Knoten „Service A“ und „Service C“.

Mikado teilt sich somit in zwei Phasen auf:

  1. Analysephase: Durch Experimente werden weitere Vorbedingungen aufgedeckt und der Baum wächst folglich. Ebenfalls können entdeckte Zyklen aufgelöst werden.
  2. Umsetzungsphase: Gefundene Blätter werden umgesetzt, wodurch der Baum schrittweise abgearbeitet wird.

Hierdurch wird das Refactoring im Vergleich zum „normalen“ Vorgehen in umgekehrter Reihenfolge durchgeführt. Dies bringt einige Vorteile:

  • Die Codebasis befindet sich immer in einem stabilen und sauberen Zustand.
  • Im Falle eines Fehlers ist sofort klar, welche Änderung diesen verursacht hat.
  • Die Abhängigkeiten und Side-Effects jedes Mikado-Knotens können einzeln analysiert werden.
  • Die Umsetzung eines Asts des Baumes ist meistens ein sinnvoller Zwischenstand, da keine „Baustellen offen sind“ und sich die Codebasis stabil bauen und testen lässt. Dieser Zwischenstand kann zurück in den Haupt-Branch gemerged werden. Das Problem von langlebigen Feature-Branches während eines Refactorings wird somit behoben.
  • Eine parallele Weiterentwicklung mit regulären Features ist weiterhin möglich.

Mikado kann wie folgt als Ablaufdiagramm dargestellt werden:

Ablaufdiagram der Mikado-Methode

Wie wir in diesem Ablaufdiagramm erkennen können, ist es völlig normal, dass wir bei der Umsetzung eines Knotens neue, vorher nicht erkannte Vorbedingungen entdecken können, obwohl wir diesen Knoten zuvor schon einmal analysiert haben. Dies entsteht immer dann, wenn Abhängigkeiten nicht sofort erkennbar sind bzw. waren.

In diesem Fall wird von der Umsetzungsphase wieder in die Analysephase gewechselt. Die gefundenen neuen Vorbedingungen des aktuellen Knotens werden wie zuvor auch notiert und die Änderungen des aktuellen Umsetzungsversuchs des Knotens werden wieder verworfen.

Durch dieses simple Vorgehen können wir große Refactorings strukturiert durchführen, ohne den Überblick zu verlieren.

Begriffsklärung Refactoring / Umbau

Die Mikado-Methode ist nicht beschränkt auf reine Refactorings. Dennoch sollten wir uns der Definition des Begriffs Refactoring bewusst sein.

Eine Refactoring verändert nur die Struktur einer Software, ohne ihr externes Verhalten zu ändern.

Im Entwickleralltag beabsichtigen wir aber meistens eine Änderung des externen Verhaltens, zum Beispiel aufgrund eines neuen Features oder einer neuen Anforderung. Genau genommen ist ein solcher Vorgang kein Refactoring, sondern ein Umbau, welcher ein oder mehrere Refactorings nötig machen kann.

Im Sinne von Mikado sind Refactorings somit Vorbedingungen des eigentlichen Umbau-Schritts. Ebenfalls können nach dem Umbau-Schritt noch weitere Refactorings notwendig sein, um eine Story in der gewünschten Form abzuschließen.

Die Mikado-Methode als Werkzeug zur Aufwandsanalyse

Das Bisherige zeigt uns, wie wir Refactorings strukturiert durchführen können. Unser Ziel ist es aber, diese im Vorfeld besser zu planen und die Aufwände besser gegenüber den Projektverantwortlichen zu begründen. Wenn wir uns die Mikado-Methode noch einmal genauer anschauen, stellen wir fest, dass es in ihr bereits eine Analysephase gibt. In dieser wird das Refactoring als gerichteter azyklischer Graph beziehungsweise als Baum visualisiert. Für unsere Aufwandsanalyse benötigen wir genau diesen Graphen. Daher müssen wir nur die Analysephase vor der eigentlichen Umsetzung der Story bzw. vor der Sprint-Planung durchführen.

Um dies zu erreichen, analysieren wir per Mikado-Methode das Refactoring in Form einer timeboxed Spike-Story. Ziel eines solchen Spikes ist nicht die Umsetzung, sondern per Mikado möglichst alle Äste und somit alle Abhängigkeiten und Probleme zu finden und zu visualisieren. Da wir während des Spikes den ganzen Baum aufdecken und visualisieren wollen, konzentrieren wir uns rein auf die Analyse und ignorieren die Umsetzungsphase. In der Regel reichen hier meistens wenige Stunden oder ein Arbeitstag. Die Analyse wird am besten zu zweit im Pair durchgeführt. Es ist aber nicht schlimm, wenn die Zeit nicht reicht und wir die Analyse nicht vollständig in der Timebox schaffen. Wir bekommen dennoch ein Gefühl, wie groß und tief der Baum bei diesem Refactoring wird. Hierfür prüfen wir am Ende des Spikes folgende Dinge:

  • Wie tief wurde der Baum bisher?
  • Wie viele Verzweigungen konnten schon identifiziert werden?
  • Konnten die tiefen Blätter in jedem Ast erreicht werden?
  • Können die Verzweigungen parallelisiert werden?
  • Wie vollständig ist der Baum?
  • Können die Verzweigungen in einzelne Stories extrahiert werden?

Wir können durch die Visualisierung der Mikado-Methode und anhand dieser Fragen den Aufwand abschätzen und gegenüber den Projektverantwortlichen begründen.

Durch die Eigenschaft, dass manche Abhängigkeiten erst bei der Umsetzungsphase entdeckt werden, bleibt ein Restrisiko bestehen. Wir haben dennoch ein klareres Bild als zuvor und können die Story entsprechend besser einplanen. Ebenfalls können wir durch die Breite des Baums mögliche Parallelisierungsmöglichkeiten aufzeigen. Meistens bilden die einzelnen Äste sinnvolle Möglichkeiten, die große Story in kleinere, besser handhabbare Stories zu zerlegen. Auch hierdurch wird das Risiko weiter minimiert.

Wir können uns den visualisierten Baum als eine Art Land- oder Schatzkarte vorstellen. Der Detailgrad und die Genauigkeit kann stark variieren, sie ist aber dennoch besser als gar keine Karte.

Test-Driven-Development

Während des Spikes können wir auch die Testabdeckung analysieren. Dadurch können entsprechend der Testgetriebenen-Entwicklung fehlende Tests im Vorfeld nachgezogen werden. Hierzu können wir neben dem normalen Coverage-Report einfach während des jeweiligen Experimentes absichtlich Fehler einbauen und prüfen, ob diese durch Tests gefunden werden. Sollten wir feststellen, dass die Testabdeckung in diesem Bereich der Anwendung unzureichend ist, können wir dies als ganz normale Mikado Vorbedingung notieren. Durch dieses Vorgehen können wir Tests an den notwendigen Stellen ergänzen, ohne gleich die ganze Anwendung mit neuen Tests zu versehen. So können sinnvolle Tests erstellt werden, welche direkt einen Mehrwert für das anstehende Refactoring bringen.

Es macht hier durchaus Sinn, die Test-Cases genau zu beschreiben. Häufig entdecken wir hierdurch unklare Anforderungen, welche wir im Vorfeld mit den Projektverantwortlichen klären können. Dies spart während des eigentlichen Refactorings Zeit, da das Entwickler-Team nicht auf die Rückmeldung des Fachbereichs warten muss.

Zusammenfassung

Durch die Mikado-Methode bekommen wir ein ideales Werkzeug, um Refactorings nicht nur strukturierter durchzuführen, sondern diese auch im Vorfeld zu analysieren, zu schätzen und aufzuteilen. Hierdurch können wir größere Refactorings und Umbauten an unserer Software durchführen ohne die Notwendigkeit von langlebigen Feature-Branches. Um dies zu erreichen, müssen wir nur die anstehenden Refactorings jeweils in einer timeboxed Spike-Story per Mikado-Methode im Vorfeld analysieren. Im Gegenzug bekommen wir ein klares Bild, was auf uns zukommt und wo mögliche Probleme liegen, welche wir durch die Visualisierung einfach und verständlich mit den Projektverantwortlichen abstimmen können.

Da es im Vorfeld meistens nicht klar ist, wie aufwendig ein Refactoring wird, macht es durchaus Sinn, jedes Refactoring mittels der Mikado-Methode zu analysieren. Im besten Fall können wir das Refactoring in der Timebox komplett abschließen. Im umgekehrten Fall sparen wir aber allen Beteiligten viel Ärger. Die Zeit ist somit nie verloren. Im Gegenteil, durch die saubere Planung von großen Refactorings sparen wir viel Zeit.

Vielen Dank fürs Lesen. Wie geht ihr mit großen Refactorings um? Was sind eure Erfahrungen?
Weitere interessante Artikel erscheinen im Blog der Digital Frontiers und werden auch auf unserem Twitter-Account angekündigt.

--

--