Blog

Refactoring: Einführung

Refactoring (Nomen), zu Deutsch Refaktorisierung:

Eine Änderung, die an der internen Struktur von Software vorgenommen wird, um sie einfacher verständlich und kostengünstiger in ihrer Erweiterbarkeit zu machen, ohne dabei das beobachtbare Verhalten zu verändern. (1)

Refactoring ist eine Disziplin, Code in strukturierter Weise aufzuräumen und dabei bestimmte Regeln anzuwenden, die darauf abzielen, das Risiko einer Funktionsstörung zu minimieren.

Inhalt des Artikels

Wenn Code entwickelt wird und im Laufe der Zeit neue Funktionen hinzukommen, dann wird er irgendwann fast zwangsläufig unübersichtlich. Unübersichtlicher Code ist schwer zu warten und das Risiko von Fehlern steigt. Die Lösung für dieses Problem ist Refactoring - das Aufräumen des Codes, ohne die Funktionalität zu verändern. In diesem Artikel erklären wir, was Refactoring bedeutet, in welchen Situationen es typischerweise durchgeführt wird und in welchen Fällen es möglicherweise nicht die beste Wahl ist.

  1. Was macht Code unübersichtlich?
  2. Warum sollten wir refaktorisieren?
  3. Wann sollten wir refaktorisieren?
  4. Warum Tests?
  5. Welche Risiken gibt es?
  6. Wann sollten wir nicht refaktorisieren?
  7. Wie überzeugen wir Interessengruppen?

Was macht Code unübersichtlich?

Code ist nicht beim ersten Mal perfekt. Selbst erfahrenste Entwickler*innen, die für die aktuelle Situation nahezu perfekten Code schreiben, werden mit sich ändernden Anforderungen von Kunden*innen oder Umgebungen konfrontiert und müssen Funktionalitäten hinzufügen oder ändern.

Code wird normalerweise iterativ entwickelt: Kommen im Laufe der Zeit neue Funktionalitäten hinzu, dann können Module und Komponenten stark voneinander abhängig werden, was Modifikationen an einzelnen Teilen herausfordernd macht.

Zeitdruck kann dazu führen, den Fokus auf das Schreiben von sauberem und strukturiertem Code zu verlieren.

Eine begrenzte Erfahrung mit bewährten Praktiken oder ein Mangel an Bewusstsein für das Schreiben von sauberem und wartbarem Code kann dazu führen, dass der Code unorganisiert ist.

Unzureichende Planung kann dazu führen, dass der Code keine klare Trennung der Anliegen (separation of concerns) aufweist. Abhängigkeiten zwischen Komponenten nehmen zu, der Code wird schwerer verständlich.

Eine ineffektive Kommunikation von Codierungsstandards und -praktiken innerhalb des Teams kann zu unterschiedlichen Codierungsstilen und Konzepten im gesamten Projekt führen. Dies kann zu Inkonsistenzen und unterschiedlichen Qualitätsstufen der Code-Struktur führen.

Das Aufschieben von Refactoring- oder Wartungsaufgaben kann zu Code führen, der schwer und teuer zu warten ist.

Warum sollten wir refaktorisieren?

Die Notwendigkeit des Refactorings bedeutet nicht, dass in der Vergangenheit Fehler gemacht wurden, sondern vielmehr, dass Code im Laufe der Zeit unstrukturierter wird. Regelmäßiges Refactoring des Codes kann Folgendes bewirken:

  • die Code-Qualität verbessern, indem duplizierter Code, lange Funktionen oder Methoden und komplexe Bedingungen entfernt werden,
  • den Code lesbarer, modularer und einfacher verständlich, sowie leichter veränderbar machen,
  • das Auffinden von Fehlern erleichtern und das Risiko von Fehlern bei Code-Änderungen verringern, was zu einem robusteren Code führt,
  • die Wartbarkeit erhöhen, indem sauberer Code geschrieben wird, der einfacher und schneller zu ändern und zu erweitern ist, sodass das Programmieren insgesamt schneller wird.

Ohne Refactoring wird es einen Zeitpunkt geben, an dem Entwickler*innen all ihre verfügbaren Ressourcen für die Fehlerbehebung aufwenden müssen, anstatt neue Funktionen hinzuzufügen. Darüber hinaus wird es sehr zeitaufwändig sein, die Funktionalität zu ändern. Erstens muss man den unübersichtlichen Code verstehen. Zweitens muss man den/die Teil(e) des Codes finden, die bearbeitet werden müssen. Und drittens wird das Risiko sehr hoch, Fehler einzuführen oder die Funktionalität zu beeinträchtigen, während man den Code ändert.

Wann sollten wir refaktorisieren?

Wir können uns an folgende Richtlinie halten, die übersetzt aus dem Englischen wie folgt lautet (2):

Die Regel der Drei

Das erste Mal, wenn du etwas tust, machst du es einfach. Das zweite Mal, wenn du etwas Ähnliches tust, zuckst du wegen der Duplizierung zusammen, aber du machst die duplizierte Sache trotzdem. Das dritte Mal, wenn du etwas Ähnliches tust, überarbeitest du es.

Für diejenigen, die Baseball mögen: Drei Schläge, dann refaktorisierst du.

1. Vorbereitendes Refactoring

Ein Ansatz besteht darin, den Code kurz vor dem Hinzufügen eines neuen Features zu refaktorisieren, um das Hinzufügen des Features zu erleichtern. Wenn der vorhandene Code nicht so strukturiert ist, dass das Hinzufügen des neuen Features einfach ist, dann gilt: "Erleichtere die Änderung, dann mache die leichte Änderung" (Kent Beck, aus dem Englischen übersetzt).

Es ist, als ob ich 100 Meilen nach Osten fahren möchte, aber anstatt einfach durch den Wald zu streifen, fahre ich 20 Meilen nach Norden zur Autobahn und dann fahre ich 100 Meilen nach Osten mit dreifacher Geschwindigkeit, die ich gehabt hätte, wenn ich direkt dorthin gegangen wäre. Wenn dich Leute drängen, einfach direkt dorthin zu gehen, musst du manchmal sagen: "Warte, ich muss die Karte überprüfen und die schnellste Route finden." Das vorbereitende Refactoring erledigt das für mich.

(Jessica Kerr, aus dem Englischen übersetzt)

2. Verständnis-Refactoring (Comprehension refactoring)

Bevor Code bearbeitet werden kann, der von einem selbst oder von jemand anderem geschrieben wurde, müssen wir verstehen, was er tut. Ein Anzeichen dafür, dass es Zeit sein könnte, den Code zu refaktorisieren, ist, wenn man nachdenken muss, um zu verstehen, was der Code tut. Sei es aufgrund einer ungeschickt strukturierten Logik oder schlecht benannter Funktionen, etc... .

Nachdem wir Logik hinter einem Codeabschnitt verstanden haben, ist ein gewisses Verständnis im unserem Kopf. Durch das Refactoring wird dieses Verständnis wieder in den Code selbst integriert.

Aufräum-Rafactoring (Litter-Pickup Refactoring) ist eine Variante des Verständnis-Refactorings: Man versteht, was der Code tut, aber er macht es schlecht, z.B. aufgrund von verschachtelter Logik oder nahezu identischen Funktionen, bei denen ein neuer Parameter den duplizierten Code entfernen könnte.

Der Kompromiss hierbei ist, dass das Umstrukturieren des Codes Zeit kosten kann, liegen gelassener Müll aber potenzielle Hindernisse für zukünftige Änderungen verursachen könnte. Wenn es einfach ist, repariere das Problem. Wenn es mehr Aufwand erfordert, mache eine Notiz und behebe es später. Eine regelmäßige Wartung der Codebasis verhindert die Ansammlung von technischen Altlasten (technical debt).

Ein altes Camping-Sprichwort besagt:

"Verlasse den Campingplatz immer sauberer, als du ihn vorgefunden hast."

3. Geplantes vs. Gelegenheits-Refactoring

Wenn Zeit für ein Refactoring nicht vorab geplant wurde, wie bei vorbereitendem, Verständnis- und Aufräum-Refactoring, dann können wir es als gelegenheitsbasiert betrachten. Es ist Teil des Programmierflusses und geschieht einfach, wenn beispielsweise ein neues Feature hinzugefügt oder ein Fehler behoben wird.

Beim Schreiben von Code gibt es immer Kompromisse:

  • Wie stark sollte parametrisiert werden?
  • Wo sollten Funktionen aufgeteilt werden?

Exzellent geschriebener Code mit richtig gewählten Kompromissen für die Features von gestern kann falsch gewählte Kompromisse für die Features von heute enthalten. Deshalb benötigt nicht nur unschöner Code ein Refactoring.

Wird sauberer und wartbarer Code geschrieben, dann sollte geplantes Refactoring selten sein. Meistens sollte Refactoring unscheinbar sein, also gelegenheitsbasiert. Oft ist der schnellste Weg, ein neues Feature hinzuzufügen, den bestehenden Code zu ändern und ihn so zu gestalten, dass das neue Feature einfach hinzugefügt werden kann. Die Anzahl der veränderten Code-Zeilen kann so leicht größer als die Anzahl der tatsächlich neu entwickelten Code-Zeilen werden.

Im Gegensatz dazu erfordert das Schreiben von hauptsächlich neuem Code, also das Betrachten von Softwareentwicklung als einen Prozess des Code-Zuwachses (process of accretion), oft große Anstrengungen beim Versuch, ein Projekt zu warten, das zu einem komplexen Objekt mit vielen Schichten gewachsen ist.

Ebenso kann das Hinauszögern oder Vernachlässigen von Refactoring zu einer verschachtelten und kaum kontrollierbaren Code-Basis führen. Dann wird dediziert geplante Zeit notwendig, um die Code-Basis in einen besseren Zustand zu versetzen, bevor neue Features hinzugefügt werden können.

4. Langfristiges Refactoring

Die meisten Refactorings dauern höchstens Minuten oder Stunden.

Einige größere können Wochen dauern, wie z.B. das Auslagern eines Codeabschnitts in ein Modul, um es mit anderen Teams zu teilen, oder das Entfernen komplexer Abhängigkeiten. In solchen Fällen ist es hilfreich, allmählich an den Änderungen im Laufe der Zeit zu arbeiten und den Code immer in einem funktionsfähigen Zustand zu lassen.

5. Refactoring während Code Reviews

Code Reviews helfen dabei,

  • Wissen zu verbreiten,
  • sind wichtig für das Schreiben von klarem Code und
  • bieten die Möglichkeit, nützliche Ideen vorzuschlagen.

Refactoring kann dazu beitragen, den Code eines anderen besser zu verstehen.

Treten während einer Code-Review einfach umzusetzende Ideen zum Refaktorisieren des Codes auf, dann lohnt es sich, zu refaktorisieren. Der Code lässt sich mit den vorgeschlagenen Änderungen testen und wir können dabei möglicherweise noch mehr Ideen entwickeln. Dies bietet konkretere Ergebnisse aus einer Code-Review. Hier kann eine sichere Vorgehensweise darin bestehen, einen neuen Branch zu erstellen, den Code zu refaktorisieren und die Änderungen mit dem Autor zu besprechen, der den zu prüfenden Code geschrieben hat, bevor irgendwelche Änderungen in den Branch des Autors übernommen werden. Um mehr Kontext zur Logik einer Implementierung zu erhalten, kann der Reviewer den Autor in den Prozess des Refactorings einbeziehen.

Der logisch nächste Schritt, bei dem der ursprüngliche Autor persönlich beteiligt ist, wird als Pair Programming bezeichnet.

Des Weiteren kann Refactoring im Rahmen von Performance-Optimierungen oder von Teamarbeit betrieben werden:

  • Refactoring kann die Leistung verbessern, indem Abhängigkeiten entfernt, die Struktur von Daten oder Algorithmen optimiert und der Ressourcenverbrauch verringert wird.
  • Gemeinsame Code-Standards können etabliert und Wissen geteilt werden.

Warum Tests?

Refactoring bedeutet, das Design einer Codebasis zu optimieren, ohne ihr beobachtbares Verhalten zu verändern. Nichts sollte kaputtgehen. Aber Fehler können auftreten. Wenn der Fehler schnell erkannt wird, kann viel Zeit bei der Fehlersuche gespart werden. Tests sind ein entscheidender Faktor, um Regressionen oder unbeabsichtigte Nebeneffekte nach einer Änderung zu identifizieren.

Wenn wir es mit einer unzureichenden Testabdeckung zu tun haben, empfiehlt es sich zunächst, Tests zu erstellen, welche die erwarteten Ergebnisse für den zu ändernden Codebereich enthalten, bevor irgendwelche Änderungen vorgenommen werden.

Best-Practice-Refactoring erfolgt in kleinen Schritten, gefolgt von Tests nach jedem Schritt. Dadurch werden Fehler schnell identifiziert und es müssen nur kleine Änderungen überprüft werden. Wenn der Fehler nicht gefunden werden kann, muss nur ein kleiner Teil des Codes zurückgesetzt werden.

Praktisch bedeutet das, es sollte möglich sein, Tests automatisch auszuführen, um nicht durch häufiges manuelles Testen entmutigt zu werden. Ohne sich-selbsttestenden Code ist die Sorge bzgl. der Einführung von Fehlern und unbeabsichtigten Nebeneffekten beim Refaktoring sehr berechtigt.

Welche Risiken gibt es?

Immer wenn Änderungen am vorhandenen Code vorgenommen werden, gibt es einige Risiken, die zu beachten sind.

Beschädigte Funktionalität: Refactoring bedeutet, dass der Code geändert wird. Obwohl dies dazu beitragen kann, Fehler zu finden, besteht auch das Risiko, Fehler einzuführen, die Funktionalität zu beschädigen oder das Verhalten eines Systems zu verändern, ohne es zu bemerken.

Teilweises oder unvollständiges Refactoring: Inkonsistente Codierungsstandards oder Code, der noch schwerer zu verstehen und zu warten ist, können das Ergebnis eines unterbrochenen Refactorings sein.

Zeitaufwändig: Beim Refactoring komplexer Systeme erfordern sorgfältige Planung, Analyse und Implementierung ggf. viel Zeit.

Unerwartete Effekte von Abhängigkeiten: Komponenten, Klassen oder Module hängen oft voneinander ab. Unbeabsichtigtes Übersehen dieser Abhängigkeiten kann zu kaskadierenden Effekten führen, die schwer zu verwalten sind.

Unbeabsichtigte Änderungen: Fehlende Erfahrung in Programmierprinzipien und fehlende Kenntnisse in Refactoring-Techniken können zu unangemessenen Änderungen führen und neue Probleme verursachen.

Solche Risiken können durch das Befolgen bewährter Praktiken erheblich reduziert werden, wie z.B.

  • schrittweises Arbeit
  • Verfolgung einer soliden Teststrategie
  • Sicherstellung einer klaren Dokumentation
  • Verfolgen der Änderungen
  • Einbeziehen erfahrener Personen beim Entwickeln

Wann sollten wir nicht refaktorisieren?

Es gibt nichts zu ändern: Sind keine neuen Features geplant und die Anwendung läuft wie erwartet, dann können wir unübersichtlichen Code ignorieren. Es hätte auch keinen Nutzen, die Anwendung besser zu verstehen. Es sollte einen bestimmten Bedarf geben, eine Anwendung zu refaktorisieren, da sonst der Aufwand und die potenziellen Risiken die Vorteile überwiegen könnten.

Enge Zeitpläne: Zeitdruck kann ein Grund sein, ein Refactoring zu verschieben. Größere Teile einer Codebasis zu refaktorisieren kann sehr zeitaufwändig werden.

Fehlende Testinfrastruktur: Wenn es nur wenige Tests gibt, müssen sie zuerst geschrieben werden. Ohne Tests kann die Korrektheit eines Refactorings kaum geprüft werden. Das Risiko, die Funktionalität zu beschädigen, wird sehr groß.

Äußerst kritische Infrastruktur: Wenn das Risiko gegenüber den Vorteilen überwiegt, sollte ein Refactoring eingehend geprüft werden. Große Änderungen an kritischem Code können auf Kosten der Systemstabilität gehen.

Einfacher neu zu schreiben: Manchmal ist es einfacher, die gesamte Anwendung neu zu schreiben, anstatt sie zu refaktorisieren. Die Entscheidung, wann refaktorisiert werden sollte und wann es besser ist, die Anwendung neu zu schreiben, erfordert Erfahrung und gutes Urteilsvermögen. Manchmal sind erste Refactoring-Versuche bzw. Versuche, die Anwendung neu zu schreiben, notwendig, um den Umfang zu verstehen. Ein komplettes Neuschreiben ist jedoch ein erheblicher Aufwand. In einer Live-Umgebung bspw. müssen während der Übergangsphase zwei Systeme aufrechterhalten werden. Die Zeit bleibt jedoch nicht stehen, und neue Funktionen müssen sowohl in das alte als auch in das neue System integriert werden. Unsere Erfahrung zeigt, dass es oft besser ist, zu refaktorisieren, anstatt alles neu zu schreiben, auch wenn der anfängliche Impuls sein mag, von Grund auf neu zu beginnen.

Wie überzeugen wir Interessengruppen?

Manchmal glauben Kund*innen oder Manager*innen, dass Refactoring Fehler aus der Vergangenheit behebt oder keine Mehrwert hat.

Technologiebewusste Manager*innen allerdings, die sich der Design-Ausdauer-Hypothese (design stamina hypothesis) bewusst sind, werden regelmäßiges Refactoring fördern und auf Anzeichen von zu wenig Refactoring achten, ohne überzeugt werden zu müssen. In der Praxis ist zu viel Refactoring viel seltener als zu wenig Refactoring.

Softwareentwicklung ist ein Beruf, und von Entwickler*innen wird erwartet, effektive und robuste Software schnell zu erstellen. Während der Entwicklung konsultieren sie Interessengruppen bspw. in der Regel nicht zu Details wie der Aufteilung von Code in Funktionen oder Dateien. Die Entscheidung für oder gegen Refactoring in einem bestimmten Fall ist ebenfalls eine Entscheidung, die selbstbestimmt von den Entwickler*innen getroffen werden sollte. Ein Prinzip des Agilen Manifests besagt:

"Die besten Architekturen, Anforderungen und Entwürfe entstehen durch selbstorganisierte Teams."

Das heißt im Wesentlichen, wenn technisches Bewusstsein auf Seiten der Interessengruppen fehlt, wird geraten, Refactoring nicht mit ihnen zu diskutieren (2). Qualität zu gewährleisten und die Möglichkeit zu haben, neue Funktionen schnell hinzuzufügen, sind sinnvolle Ziele und nur einige der Vorteile von Refactoring. Nur Code, der leicht zu lesen und zu verstehen ist, kann effizient erweitert werden. Refactoring muss Teil der kontinuierlichen Entwicklung sein.

Literature

(1) https://martinfowler.com/bliki/DefinitionOfRefactoring.html

(2) Refactoring: Improving the Design of Existing Code. M. Fowler. Addison-Wesley, Boston, MA, USA, (2019).