Refactoring (auch Refaktorisierung, Refaktorierung oder Restrukturierung) bezeichnet in der Software-Entwicklung die manuelle oder automatisierte Strukturverbesserung von Quelltexten unter Beibehaltung des beobachtbaren Programmverhaltens. Dabei sollen Lesbarkeit, Verständlichkeit, Wartbarkeit und Erweiterbarkeit verbessert werden, mit dem Ziel, den jeweiligen Aufwand für Fehleranalyse und funktionale Erweiterungen deutlich zu senken.
Refactoring ist ein zentraler Bestandteil der Agilen Softwareentwicklung. Dort wird meist von „kontinuierlichem“ Refactoring[1] oder „kompromisslosem“ Refactoring[2] gesprochen. Refactoring ist in der agilen Softwareentwicklung wie Kodieren oder Modultesten ein integraler Bestandteil des Softwareentwicklungsprozesses und nicht auf bestimmte Zeiten bzw. Phasen beschränkt.
Der Begriff wurde zum ersten Mal in einer Arbeit von Ralph Johnson und William Opdyke 1990 gebraucht (Refactoring: An aid in designing application frameworks and evolving object-oriented systems. In: Proceedings of Symposion on Object-Oriented Programming Emphasizing Practical Applications (SOOPPA), September 1990). Opdyke promovierte 1992 zu dem Thema.
Sie entwickelten die Idee einer Software-Refactory, die das Umgestalten (eben das Refactoring) von Computerprogrammen erleichtern sollte.
Die unzutreffende Übersetzung Refaktorisierung stammt aus einer Verwechslung mit einer häufig zitierten Analogie, die ursprünglich nicht Begriffsinhalt war: Refactoring ist eine Art, ein Programm so zu modifizieren, dass verborgene Strukturen offengelegt werden, ohne die Funktionalität zu ändern. Dies, so der (fälschliche) Analogieschluss, entspreche dem Vorgehen der Faktorisierung von Polynomen in der Mathematik.
Refactoring wird hauptsächlich auf unschöne Stellen im Code (siehe Code-Smell) angewandt. Dabei wird der Quelltext eines Computerprogramms umgestaltet, wobei die tatsächliche Programmfunktion unverändert bleiben soll. Die Umgestaltung des Quelltextes erfolgt meist nach folgenden Gesichtspunkten:
Die Gesichtspunkte des Refactorings hängen eng mit den daraus resultierenden Vorteilen zusammen. Das Refactoring hat ein Analogon in der Mathematik in einer Vorgehensweise, die als algebraische Umformung bezeichnet wird, bei der das Ziel der Umformung ebenfalls eine bessere Lesbarkeit, Verständlichkeit und gegebenenfalls Erweiterbarkeit (des Gleichungssystems) ist. Aus diesem Grunde sind funktionale Sprachen (Lisp, Haskell, OCaml, Erlang und so weiter) wesentlich besser geeignet, ein Refactoring durchzuführen, da sie auf einem mathematischen Paradigma der Programmierung basieren.
Das Refactoring wird erleichtert und unterstützt durch:
Mögliche Refactorings
Folgende Maßnahmen oder Arbeiten werden beim Refactoring besonders häufig durchgeführt:
Vorteile
Refactoring dient der Verbesserung der Wartbarkeit des Designs in der Art, dass es für den Programmierer leichter wird, den bestehenden Code funktional zu erweitern oder an anderer Stelle wiederzuverwenden. Dies versucht man zu erreichen, indem man den Code insbesondere bezüglich folgender Kriterien verbessert:
Im üblichen Softwareentwicklungszyklus ist ein fortwährender Kreislauf von Spezifikation, Design, Implementierung und Tests vorgesehen. Nach jedem Durchlauf kann das Softwareprodukt immer wieder neu in diesen Kreislauf einsteigen. Mit den klassischen Techniken hieß das jedoch, dass nach einer Änderung der Spezifikation oder einem Redesign oft Teile oder sogar das ganze Programm völlig neu geschrieben werden mussten. Refactoring erlaubt dem Entwickler, diesen Zyklus permanent im Kleinen ablaufen zu lassen, und so sein Produkt kontinuierlich zu verbessern.
Nachteile
Je nach Umsetzung kann Refactoring auch einige Nachteile mit sich ziehen:
Refactoring wird nur auf funktionierendem Code ausgeführt (dessen Funktionalität erhalten bleiben soll). Dies beinhaltet aber auch das Risiko ungewünschter Änderungen und Fehler. Um dieses Risiko zu vermeiden (oder wenigstens zu minimieren) verwendet man verschiedene Regeln, die den Prozess des Refaktorisierens weniger gefährlich machen.
Zuerst sollte man eine Reihe automatisch ablaufender Unit-Tests haben. Diese werden vor dem Refactoring angewandt, und man beginnt erst, wenn die Tests alle funktionieren. Zusätzlich sollte mit Hilfe eines geeigneten Programms die Testabdeckung ermittelt und geprüft werden, ob die zu ändernde Stelle im Code tatsächlich durch automatisierte Tests geschützt ist. Dies stellt sicher, dass das Programm richtig läuft. Nach Ausführung des Refactoring wird wieder die Testsuite ausgeführt. So kann man einige Fehler beim Refactoring sofort erkennen. Falsch wäre jedoch die Aussage, dass Unit-Tests das Refactoring sicher machen könnten, Unit-Tests senken lediglich die Risiken des Refactorings.
Weiterhin gilt das Prinzip der kleinen Änderungen. Wenn man nur wenig verändert, so kann man zum einen hoffen, auch nur wenig zu zerstören, falls man durch das Refactoring Fehler einträgt (trotzdem können kleine Ursachen große Auswirkungen haben). Zum anderen lassen sich gemachte Fehler dann auch leichter finden. Meistens kann man komplexe Refactorings, die man plant, in einfache kleine Einheiten zerlegen. Vor und nach jedem Schritt wird wieder durch die Tests die Integrität des Systems geprüft. Durch die Verwendung automatisierter Refactoring-Funktionen (wie sie z. B. von Eclipse oder Borland Delphi ab Version 2005 zur Verfügung gestellt werden) lassen sich ebenfalls Fehlerquellen effektiv ausschließen sowie der eigene Arbeitsaufwand minimieren.
Schließlich gibt es einen Katalog von Refactoring-Mustern, die ähnlich wie die Entwurfsmuster eingesetzt werden, um Fehler zu vermeiden. Dabei ist in jedem Muster eine Reihe von Parametern definiert. Da wäre erstmal das Ziel des Musters (Methode extrahieren, Klasse umbenennen etc.) und dazu dann eine Reihe von Arbeitsanweisungen, die für diese Aktion ausgeführt werden müssen. Viele dieser Muster können heutzutage automatisch von Werkzeugen umgesetzt werden. Man trifft als Softwareentwickler nur noch die Entscheidung, welches Muster worauf angewendet wird, um den Quelltext zu verbessern. Es ist jedoch zu beachten, dass die Mechanismen oftmals noch recht fehleranfällig sind. Im besten Fall kommt es durch so verursachte Fehler zu einem Problem beim Übersetzen, aber auch Laufzeitfehler können die Folge sein. Ein umfangreiches, möglichst automatisiertes Testen ist daher nach einem Refactoring immer erforderlich.
Dieser Java-Code vor dem Refactoring enthält eine temporäre Variable, die für mehrere Zwecke verwendet wird und einen nichtssagenden Namen besitzt:
double x = 2 * (breite + hoehe);
System.out.println("Umfang: " + x);
x = breite * hoehe;
System.out.println("Fläche: " + x);
Durch Refactoring wird für jeden der Verwendungszwecke eine getrennte Variable deklariert, die jeweils einen aussagekräftigen Namen trägt:
double umfang = 2 * (breite + hoehe);
System.out.println("Umfang: " + umfang);
double flaeche = breite * hoehe;
System.out.println("Fläche: " + flaeche);
Durch weiteres Refactoring können die beiden lokalen Variablen eliminiert werden.
Nachteile:
Der entstehende Code wird weder besser noch schlechter, da Compiler seit Mitte der 1990er Jahre Common subexpression elimination wie auch Live variable analysis beherrschen.
System.out.println("Umfang: " + (2 * (breite + hoehe)));
System.out.println("Fläche: " + (breite * hoehe));
Man könnte die Berechnung auch in eine Klasse verlegen und diese verwenden:
Rechteck rechteck = new Rechteck(breite, hoehe);
System.out.println("Umfang: " + rechteck.umfang() );
System.out.println("Fläche: " + rechteck.flaeche() );
System.out.println("Eckenanzahl: " + rechteck.ecken() );
System.out.println("Diagonalen: " + rechteck.diagonale(0,1) );
Werkzeuge