Refactoring: Wie sich Quellcode verbessern lässt

Im Laufe der Entwicklung einer Anwendung sammeln sich in Quellcodes unsauber strukturierte Stellen, die die Anwendbarkeit und Kompatibilität eines Programms gefährden. Die Lösung ist entweder ein komplett neuer Quellcode oder eine Restrukturierung in kleinen Schritten. Viele Programmierer und Unternehmen entscheiden sich zunehmend für das Code-Refactoring, um eine funktionierende Software langfristig zu optimieren und für andere Programmierer lesbarer und klarer zu gestalten.

Beim Refactoring stellt sich die Frage, welches Problem im Code durch welche Methode gelöst werden soll. Inzwischen gehört Refactoring zu den Grundlagen beim Programmieren lernen und gewinnt immer mehr an Bedeutung. Welche Methoden kommen dabei zur Anwendung und welche Vorteile und Nachteile entstehen?

Was ist Refactoring?

Die Programmierung einer Software ist ein langwieriger Prozess, an dem teilweise mehrere Entwickler arbeiten. Dabei wird geschriebener Quelltext oft überarbeitet, umgestellt und erweitert. Durch Zeitdruck oder veraltete Praktiken sammeln sich unsaubere Stellen in Quellcodes, sogenannte Code-Smells. Diese historisch gewachsenen Schwachstellen gefährden die Anwendbarkeit und Kompatibilität eines Programms. Um die voranschreitende Erosion und Verschlechterung einer Software zu vermeiden, ist ein Refactoring notwendig.

Das Refactoring ist mit dem Lektorieren eines Buches vergleichbar. Während des Lektorats entsteht kein vollkommen neues Buch, sondern ein besser verständlicher Text. So wie es beim Lektorieren verschiedene Herangehensweisen wie Kürzen, Umformulieren, Streichen oder Umstrukturieren gibt, so gibt es auch beim Code-Refactoring Methoden wie Einkapseln, Umformatieren oder Extrahieren, um einen Code zu optimieren, ohne ihn im Wesen zu verändern.

Dieser Prozess ist deutlich kostengünstiger als die Erstellung einer komplett neuen Codestruktur. Vor allem in der iterativen und inkrementellen Software-Entwicklung sowie der agilen Software-Entwicklung spielt Refactoring eine große Rolle, da Programmierer in diesem zyklischen Modell eine Software immer wieder ändern. Refactoring ist dabei ein fester Arbeitsschritt.

Wenn ein Quellcode erodiert: Spaghetti-Code

Zunächst ist es nötig zu verstehen, wie ein Code altern und zum berüchtigten Spaghetti-Code mutieren kann. Ob aus Zeitdruck, durch mangelnde Erfahrung oder unklare Anweisungen: Die Programmierung eines Codes führt durch unnötig komplizierte Befehle zu Einbußen in der Funktionalität. Ein Code erodiert zunehmend, je schneller und komplexer sein Anwendungsbereich ist.

Spaghetti-Code steht für einen verworrenen, unlesbaren Quellcode, dessen Sprache für Programmierer nur schwer verständlich ist. Einfache Beispiele für verworrenen Code sind überflüssige Sprung-Befehle (GOTO), die ein Programm anweisen, im Quellcode hin und her zu springen, oder überflüssige for-/while-Schleifen und if-Anweisungen.

Besonders Projekte, an denen viele Software-Entwickler arbeiten, neigen zu unübersichtlichem Quelltext. Geht ein Code durch viele Hände und enthielt das Original bereits Schwachstellen, so lässt sich eine zunehmende Verknotung durch „Umschiffungslösungen“ und ein kostspieliges Code-Review schwer vermeiden. In den drastischsten Ausprägungen kann Spaghetti-Code die komplette Entwicklung einer Software gefährden. Dann kann selbst Code-Refactoring das Problem nicht beheben.

Nicht ganz so drastisch sind Code-Smells und Code-Rot. Mit dem Alter kann ein Code durch unsaubere Stellen im übertragenen Sinne zu riechen anfangen. Schlecht verständliche Stellen verschlimmern sich durch Eingriffe anderer Programmierer oder Erweiterungen. Findet bei ersten Anzeichen von Code-Smell kein Refactoring statt, so erodiert der Quellcode zusehends und verliert durch Code-Rot (von engl. rot für „verrotten“) seine Funktionalität.

Was bezweckt ein Refactoring?

Die Absicht hinter einem Refactoring ist schlicht und einfach ein besserer Code. Durch einen effektiven Code lassen sich neue Code-Elemente besser integrieren, ohne neue Fehler einzubauen. Programmierer, die einen Code mühelos lesen können, finden sich schneller ein und können Bugs leichter entfernen oder vermeiden. Ein anderer Zweck des Refactorings ist eine verbesserte Fehleranalyse und Wartbarkeit einer Software. Programmierer, die einen Code überprüfen, haben deutlich weniger Aufwand.

Welche Fehlerquellen behebt Refactoring?

Die Techniken, die beim Refactoring zum Einsatz kommen, sind so vielfältig wie die Fehler, die sie beheben sollen. Im Grunde definiert sich Code-Refactoring durch seine Fehler und zeigt die nötigen Schritte, um einen Lösungsweg zu verkürzen oder zu entfernen. Fehlerquellen, die Sie durch Methoden des Refactorings beheben können, sind u. a.:

  • Verworrene oder zu lange Methoden: Befehlsketten und -blöcke sind so lang, dass Außenstehenden die innere Logik der Software unverständlich ist.
  • Code-Dopplungen (Redundanzen): Ein unübersichtlicher Code enthält oftmals Redundanzen, die bei Wartungen an jeder Stelle separat geändert werden müssen und damit zeit- und kostenintensiv sind.
  • Zu lange Parameterlisten: Objekte werden nicht direkt an eine Methode gegeben, sondern ihre Attribute in einer Parameterliste übermittelt.
  • Klassen mit zu vielen Funktionen: Klassen mit zu vielen als Methode definierten Funktionen, auch als Gott-Objekte bezeichnet, die eine Anpassung der Software fast unmöglich machen.
  • Klassen mit zu wenigen Funktionen: Klassen mit so wenigen als Methode definierten Funktionen, dass sie überflüssig sind.
  • Zu allgemeiner Code mit Spezialfällen: Funktionen mit zu spezifischen Sonderfällen, die kaum oder nicht eintreten und daher das Hinzufügen von notwendigen Erweiterungen erschweren.
  • Middle Man: Eine separate Klasse fungiert als Vermittler zwischen Methoden und verschiedenen Klassen, statt Aufrufe von Methoden direkt an eine Klasse zu leiten.

Wie ist die Vorgehensweise bei Refactoring?

Refactoring sollte stets vor der Änderung einer Programmfunktion kommen. Es erfolgt am besten in sehr kleinen Schritten und testet Änderungen des Codes durch Software-Entwicklungsprozesse wie Test Driven Development (TDD) und Continuous Integration (CI). Kurz gesagt stehen TDD und CI für das kontinuierliche Testen von neuen, kleinen Codeabschnitten, die Programmierer bauen, integrieren und durch oft automatisierte Testdurchläufe auf ihre Funktionalität hin prüfen.

Es gilt die Regel: Das Programm nur in kleinen Schritten von innen ändern, ohne die äußere Funktion zu beeinflussen. Nach jeder Änderung sollten Sie einen möglichst automatisierten Testlauf durchführen.

Welche Techniken gibt es?

Es gibt zahlreiche konkrete Refactoring-Techniken. Eine vollständige Übersicht findet sich in dem umfangreichsten Werk zum Refactoring von Martin Fowler und Kent Beck: „Refactoring: Improving the Design of Existing Code“. Hier ein kurzer Überblick:

Rot-Grün-Entwicklung

Die Rot-Grün-Entwicklung ist eine testgesteuerte Methode der agilen Software-Entwicklung. Sie findet Anwendung, wenn man eine neue Funktion in einen bestehenden Code integrieren möchte. Rot steht für den ersten Testdurchlauf vor der Implementierung einer neuen Funktion in den Code. Grün steht für den einfachsten möglichen Codeabschnitt, der für die Funktion nötig ist, um den Test zu bestehen. Was folgt, ist eine Erweiterung mit konstanten Testdurchläufen, um fehlerhaften Code auszusortieren und die Funktionalität zu erhöhen. Rot-Grün-Entwicklung ist ein Grundstein für kontinuierliches Refactoring bei kontinuierlicher Software-Entwicklung.

Branching-by-Abstraction

Diese Refactoring-Methode beschreibt eine schrittweise Änderung an einem System und die Umstellung alter implementierter Codestellen auf neue integrierte Abschnitte. Branching-by-Abstraction kommt gewöhnlich bei großen Änderungen zum Einsatz, die Klassenhierarchien, Vererbung und Extrahierung betreffen. Durch die Implementierung einer Abstraktion, die mit einer alten Implementierung verknüpft bleibt, lassen sich andere Methoden und Klassen mit der Abstraktion verknüpfen und die Funktionalität des alten Codeabschnitts durch die Abstraktion ersetzen.

Oft geschieht dies durch Pull-up- oder Push-down-Methoden. Sie verknüpfen eine neue, bessere Funktion mit der Abstraktion und übertragen die Verknüpfungen auf diese. Dabei verschieben Sie entweder eine Unterklasse in eine höhere Klasse (Pull-up) oder teilen eine höhere Klasse in Unterklassen auf (Push-down).

Die alten Funktionen können Sie schließlich löschen, ohne die Gesamtfunktionalität zu gefährden. Durch diese kleinteiligen Änderungen funktioniert das System unverändert, während Sie unsauberen Code Abschnitt für Abschnitt durch sauberen Code ersetzen.

Methoden zusammenstellen

Refactoring soll die Methoden eines Codes so leicht lesbar wie möglich machen. Bereits beim Lesen erschließt sich im besten Fall die innere Logik einer Methode auch außenstehenden Programmierern. Für das effiziente Zusammenstellen von Methoden gibt es beim Refactoring verschiedene Techniken. Ziel jeder Änderung ist es, Methoden zu vereinheitlichen, Dopplungen zu entfernen und zu lange Methoden in eigene Abschnitte zu spalten, um sie für zukünftige Änderungen zu öffnen.

Techniken dafür sind beispielsweise:

  • Methodenextrahierung
  • Methode inline setzen
  • Entfernung temporärer Variablen
  • Temporäre Variablen durch Anfragemethode ersetzen
  • Einführung beschreibender Variablen
  • Trennung temporärer Variablen
  • Entfernung von Zuweisungen an Parametervariable
  • Methode durch ein Methoden-Objekt ersetzen
  • Algorithmus ersetzen

Eigenschaften zwischen Klassen verschieben

Um einen Code zu verbessern, müssen Sie mitunter Attribute oder Methoden zwischen Klassen verschieben. Hierfür kommen folgende Techniken zur Anwendung:

  • Verschiebe Methode
  • Verschiebe Attribut
  • Extrahiere Klasse
  • Setze Klasse inline
  • Verstecke den Delegate
  • Entferne Klasse in der Mitte
  • Führe fremde Methode ein
  • Führe lokale Erweiterung ein

Datenorganisation

Diese Methode hat das Ziel, Daten in Klassen zu unterteilen und diese möglichst klein und übersichtlich zu halten. Unnötige Verknüpfungen zwischen Klassen, die bei kleinsten Veränderungen die Funktionalität der Software beeinträchtigen, sollten Sie entfernen und in schlüssige Klassen teilen.

Beispiele für Techniken sind:

  • Kapseln eigener Attributzugriffe
  • Eigenes Attribut durch Objektverweis ersetzen
  • Wert durch Referenz ersetzen
  • Verweis durch Wert ersetzen
  • Verkoppelung beobachtbarer Daten
  • Kapselung von Attributen
  • Datensatz durch Datenklasse ersetzen

Vereinfachung bedingter Ausdrücke

Bedingte Ausdrücke sollten Sie während des Refactorings so weit wie möglich vereinfachen. Hierzu bieten sich folgende Techniken an:

  • Bedingungen zerlegen
  • Zusammenführen von bedingten Ausdrücken
  • Zusammenführen wiederholter Anweisungen in bedingten Ausdrücken
  • Entfernen von Kontrollschaltern
  • Ersetzen geschachtelter Bedingungen durch Wächter
  • Fallunterscheidungen durch Polymorphie ersetzen
  • Einführen von Null-Objekten

Vereinfachung von Methodenaufrufen

Methodenaufrufe lassen sich u. a. durch folgende Methoden schneller und leichter durchführen:

  • Methoden umbenennen
  • Parameter hinzufügen
  • Parameter entfernen
  • Parameter durch explizite Methode ersetzen
  • Fehlercodes durch Ausnahmen ersetzen

Refactoring-Beispiel: Methoden umbenennen

Im folgenden Beispiel ist erkennbar, dass im ursprünglichen Code die Benennung der Methode deren Funktionalität nicht eindeutig und schnell verständlich macht. Die Methode soll die Postleitzahl einer Büroanschrift ausgeben, zeigt diese Aufgabe jedoch nicht direkt im Code an. Um den Code klarer zu formulieren, bietet sich beim Code-Refactoring eine Umbenennung der Methode an.

Vorher:

String getPostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getPostalCode());

Nachher:

String getOfficePostalCode() {
	return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getOfficePostalCode());

Refactoring: Welche Vorteile und Nachteile hat es?

Vorteile Nachteile
Bessere Verständlichkeit erleichtert die Wartung und Erweiterbarkeit der Software. Ein ungenaues Refactoring könnte neue Bugs und Fehler in den Code implementieren.
Die Restrukturierung des Quellcodes ist ohne Veränderung der Funktionalität möglich. Es gibt keine klare Definition für einen „sauberen Code“.
Verbesserte Lesbarkeit erhöht die Verständlichkeit des Codes für andere Programmierer. Ein verbesserter Code ist für den Kunden oft nicht erkennbar, da die Funktionalität identisch bleibt, d. h. der Nutzen ist nicht offensichtlich.
Beseitigte Redundanzen und Dopplungen erhöhen die Effektivität des Codes. Bei größeren Teams, die am Refactoring arbeiten, könnte der Abstimmungsaufwand unerwartet hoch sein.
In sich geschlossene Methoden verhindern, dass lokale Änderungen auf andere Teile des Codes Auswirkungen haben.  
Ein sauberer Code mit kürzeren, in sich geschlossenen Methoden und Klassen zeichnet sich durch bessere Testbarkeit aus.  

Grundsätzlich gilt beim Refactoring: Führen Sie neue Funktionen nur ein, wenn der vorliegende Quellcode unverändert bleibt. Nehmen Sie Veränderungen am Quellcode, also Refactoring, nur vor, wenn Sie keine neuen Funktionen hinzufügen.