Bild von Björn Geisemeyer
Björn Geisemeyer

Refactoring in Phasen

Refactorings sind ein fester Bestandteil unseres Entwicklerlebens. Es wäre schön, immer auf der grünen Wiese beginnen zu können, neue Features zu implementieren und neue Ideen umzusetzen. Doch viel häufiger sind wir damit konfrontiert, vorhandenen Quelltext nachzujustieren. In diesem Kontext spielen Refactorings eine entscheidende Rolle, besonders wenn wir an Legacy Code denken. Das Ergänzen einer Anforderung oder das Beseitigen von Fehlern ist oft mit der Durchführung eines Refactorings verbunden. Häufig bekommen wir die Aufgabe, in einer ungetesteten Codebasis aufzuräumen, was viele Entwickler scheuen.

Ein Beispiel aus meinen frühen Entwicklerjahren sieht ungefähr so aus (von KI leicht ausgeschmückt):
Eines Tages kam mein Chef zu mir mit einer heiklen Mission – ich sollte ein Refactoring durchführen, das ein neues Feature nahtlos integrierbar machen sollte. Er gab mir genau eine Woche Zeit und sah mich erwartungsvoll an: „Kannst du das schaffen?“ Schnell überflog ich den Code, nickte und sagte zu. Ich legte einen Refactoring-Branch an, startete meine IDE und machte mich an die Arbeit.

Anfangs schien alles nach Plan zu verlaufen. Nur Testen war nicht gerade meine Paradedisziplin zu diesem Zeitpunkt und so blieb der Code fast so ungetestet, wie zuvor. Was zunächst wie ein wohlstrukturiertes Unterfangen begann, verwandelte sich bald in ein Minenfeld. Unerwartete Compilerfehler explodierten links und rechts, unerkannte Verhaltensänderungen schlichen sich ein, und versteckte Codeabhängigkeiten tauchten auf, wie Gespenster aus dem Nichts. Nachdem die erste Woche vergangen war, hatte ich die Deadline bereits gerissen. Das löste eine Welle von Stress und Nervosität aus. Ich programmierte unter hohem Druck, während mir der Schweiß über die Stirn rann, getrieben von dem verzweifelten Ziel, keine weitere kostbare Zeit zu verlieren. Was als einwöchiges Projekt geplant war, zog sich qualvoll über drei Wochen hin, und ich war völlig am Ende meiner Kräfte, als endlich alles in den Trunk gemerged wurde.

Klingt das vertraut? Glücklicherweise muss Refactoring nicht chaotisch sein. Es gibt zahlreiche Bücher und entwickelte Methoden, die uns helfen, eine geordnete Herangehensweise zu wählen. Letztendlich geht es darum, eine Struktur zu schaffen, mit der ein Refactoring planbar, vorhersehbar und überlegt durchgeführt werden kann – ganz ohne Bauchschmerzen. In diesem Beitrag erkläre ich, wie du dieses Ziel in vier Schritten erreichen kannst.

1. Analyse

Die Analyse ist die erste Phase eines Refactorings, mit der du sicherlich vertraut bist. Ohne eine Analyse ist ein Refactoring nicht möglich. In dieser Phase setzen wir uns intensiv mit dem Code auseinander, um den Umfang der bevorstehenden Aufgaben zu erfassen. Diese Phase verfolgt drei Hauptziele.

  1. Zunächst geht es darum, sich mit dem Code vertraut zu machen. Unabhängig davon, ob die Codebasis uns bekannt ist oder nicht, müssen wir uns Zeit nehmen, um ihre gegenwärtige Struktur zu verstehen.
  2. Haben wir den Code verstanden, widmen wir uns dem zweiten Ziel: das Finden eines geeigneten Einstiegspunkts. Ein komplexes Refactoring muss begründet sein, denn es ist mit Kosten verbunden, erzeugt aber selbst keinen produktiven Mehrwert. Die zwei Gründe, ein Refactoring durchzuführen, sind die Implementierung eines neuen Features oder die Behebung eines Fehlers. Für beide Szenarien finden wir in der Codebasis eine Stelle für den Einstieg der Implementation.
  3. Sobald der Startpunkt festgelegt ist, gehen wir zum dritten Ziel über: die Festlegung des Rahmens für das Refactoring. Dieser Rahmen kann eine Methode, eine Klasse oder auch ein umfangreicheres Modul umfassen, je nach den Anforderungen an das Refactoring. Dieser Rahmen ist unser Ausgangspunkt für die weiteren Schritte.

Soweit sollte das Vorgehen bekannt sein, denn ohne die Analyse können wir nicht einmal ein unstrukturiertes Refactoring durchführen. Wir können mit der Implementierung nicht beginnen, bevor wir nicht wissen, wo wir anfangen und was wir aufräumen möchten

2. Unter Test stellen

Warum ist es wichtig, Code vor einem Refactoring unter Test zu stellen? Betrachten wir Michael Feathers‘ ursprüngliche Definition von Legacy Code: „Legacy Code ist ungetesteter Code“. Eine einfache Aussage und gleichzeitig die größte Herausforderung, wenn wir innerhalb solchen Codes ein Refactoring durchführen möchten. Das grundlegende Prinzip eines Refactorings ist: Vorhandenes Verhalten darf nicht verändert werden! Wie garantiere ich das? Mit Tests. Darum müssen wir den gesamten Codeausschnitt, der einem Refactoring unterzogen werden soll, ausreichend unter Test stellen. Oftmals begegnen wir dabei einem Dilemma: Der Code hat eine Struktur, die das Erstellen einfacher Tests (nahezu) unmöglich macht. So braucht es Refactorings, um den Code in eine testbare Struktur zu bringen. Gleichzeitig gilt, dass ungetesteter Code nicht nicht refaktorisiert werden sollte. Glücklicherweise gibt es verschiedene Ansätze, die uns unterstützen können.

Das Testen komplexer Funktionen in Legacy Code mittels Standard-Unit-Tests kann herausfordernd sein. Um effektive Unit-Tests erstellen zu können, müssen wir das Verhalten verstehen, das die Methode abbildet. Ein Unit-Test ist nur dann überprüfbar, wenn wir bereits wissen, welches Ergebnis bei gegebenen Testdaten zu erwarten ist. Eine praktikable Alternative sind Approval-Tests. Diese sind insbesondere bei Legacy Code oft deutlich einfacher zu implementieren. Plus: Wir müssen das getestete Verhalten nicht einmal kennen. In diesem Artikel stelle ich Approval-Tests vor.

Durch den Einsatz bestimmter Techniken und IDE-unterstützter Refactoring-Methoden, lässt sich schwer testbarer Code Stück für Stück auflösen. Der Einsatz ausschließlich IDE-gestützter Methoden garantiert dabei eine sehr geringe Gefahr, das vorhandene Verhalten versehentlich zu ändern. Die isolierten Elemente können dann getestet werden. Hier ist eine beeindruckende Demonstration von Llewellyn Falco zu diesem Vorgehen.

Eine weitere Möglichkeit ist das Testen, ohne direkt im Code arbeiten zu müssen. Systemtests, auch Ende-zu-Ende-Tests genannt, prüfen die Anwendung durch das UI. Sie simulieren über die Benutzeroberfläche einen Use Case – eine spezifische Anforderung, die der Nutzer an das System stellt – und verifizieren das daraus resultierende Ergebnis. Können wir für den zu refaktorisierenden Codeausschnitt keine Unit- oder Integrationstests erstellen, müssen wir ermitteln, welche Use Cases durch diese Logik beeinflusst werden. Diese Use Cases sollten dann mit Ende-zu-Ende-Tests abgedeckt werden. Das kann ein anspruchsvolles Unterfangen sein, Ende-zu-Ende-Tests sind zeitintensiv. Dafür erhalten wir jedoch die Gewissheit, dass das Refactoring die betroffenen Use Cases nicht verändert. Und seien wir ehrlich: der Aufwand entsteht jetzt, weil wir vorher nicht sauber gearbeitet haben und auf Tests verzichtet haben!

3. Experimente

Die dritte Phase verhilft uns zu einer Refactoring Struktur. Durch Experimente an dem bereits analysierten und getesteten Codeausschnitt ermitteln wir den tatsächlichen Umfang des benötigten Refactorings. Diese Experimente führen wir nach einer bestimmten Methode aus, bekannt als Mikado-Methode. Ziel dieser Methode ist es, alle Abhängigkeiten zu identifizieren, die mit unserem geplanten Refactoring verbunden sind. Durch die Experimente decken wir Herausforderungen auf, die wir in der Analysephase womöglich übersehen haben. Gleichzeitig prüfen wir, ob unsere Refactoring-Strategie zu einem guten Ergebnis führt. Bei Bedarf passen wir die Strategie entsprechend an. Als Ergebnis entsteht ein detaillierter Graph, der jeden einzelnen Bestandteil und Schritt unseres Refactorings visualisiert. Dieser Graph dient als Grundlage für unsere Implementierung. Stefan Lieser gibt in diesen Videos Einblick in das Vorgehen der Methode.

4. Implementation

Der Mikado-Graph eröffnet neue Möglichkeiten für die Gestaltung der Implementation. Durch die vorherigen Experimente arbeiten wir nicht mehr mit unbekanntem Quelltext. Außerdem können wir den Gesamtumfang des Refactorings besser einschätzen. Jeder Schritt lässt sich separat hinsichtlich des zeitlichen Aufwands bewerten. Da bereits eine experimentelle Implementierung stattgefunden hat, können wir die benötigte Zeit realistischer einschätzen.

Die Struktur ermöglicht uns auch, unterschiedliche Implementationsstrategien anzuwenden. Die Implementation kann in einem durchgehenden Prozess erfolgen oder in Teilschritte gegliedert werden. Steht das Refactoring unter Zeitdruck, bietet sich die Option an, mehrere Entwickler parallel einzusetzen und so die Implementierung zu skalieren. Alternativ kann die Umsetzung auch über einen längeren Zeitraum gestaffelt stattfinden, um zeitgleich mehr Kapazitäten für andere Aufgaben bereitstellen zu können. Struktur bedeutet Entscheidungen treffen zu können, statt auf Unvorhergesehenes reagieren zu müssen.

Fazit

Entwickler machen Refactorings und arbeiten häufig mit Legacy Code. Für viele ist die grüne Wiese nur ein ferner Traum. Im Legacy Code darf kein Platz für chaotische Herangehensweisen sein. Mein überspitztes Szenario in der Einleitung ist vielen vertraut und allgemein unbeliebt. Zum Glück benötigt es nur ein wenig Struktur, um diesem Szenario zu entgehen. Diese vier „kleinen“ Schritte machen den entscheidenden Unterschied. Die Methoden und Werkzeuge, die ich hier vorgestellt habe, sind leicht in einen Refactoring-Prozess zu integrieren und ermöglichen ein planbares, vorhersehbares und strukturiertes Vorgehen. Adíos Bauchschmerzen.

Neben den Links im Beitrag möchte ich dir gerne unsere Buchempfehlungen zum Thema Refactoring mitgeben. Die dort vertretenen Werke sind eine hervorragende Grundlage!

Für alle, die sich etwas Anleitung und Praxisübungen wünschen, empfehle ich unsere Seminare. Setzte dich gerne mit uns in Verbindung und wir gestalten gemeinsam ein auf euch zugeschnittenes Training.

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_DEGerman