Umstrukturierung bestehenden Quellcodes
Code-Refactoring ist der Prozess der Umstrukturierung bestehenden Quellcodes ohne dessen externes Verhalten zu ändern. Refactoring verbessert keine funktionalen Attribute der Software. Die Vorteile von Refactoring umfassen verbesserte Lesbarkeit des Codes und reduzierte Komplexität. Dadurch kann oft die Wartbarkeit des Quellcodes verbessert und eine ausdrucksstärkere interne Architektur oder ein Objektmodell zur Verbesserung der Erweiterbarkeit erstellt werden.
Typischerweise wendet das Refactoring eine Reihe von standardisierten grundlegenden Mikrorefactorings an, von denen jedes (normalerweise) eine winzige Änderung im Quellcode eines Computerprogramms ist, die entweder das Verhalten der Software bewahrt oder zumindest ihre Konformität mit funktionalen Anforderungen nicht modifiziert.
Viele Entwicklungsumgebungen bieten eine automatisierte Unterstützung für die Durchführung der mechanischen Aspekte dieser grundlegenden Refactorings. Wenn das Code-Refactoring sehr gut funktioniert, kann es Softwareentwicklern helfen, verborgene oder ruhende Fehler oder Sicherheitslücken im System zu entdecken und zu beheben, indem die zugrunde liegende Logik vereinfacht und unnötige Komplexitätsstufen vermieden werden. Wenn Refactorings weniger gut ausgeführt werden, kann unter Umständen die Anforderung, dass die externe Funktionalität nicht geändert wird und keine neuen Bugs entstehen nicht erfüllt werden.
Wie funktioniert Code Refactoring?
Indem wir das Design von Code kontinuierlich verbessern, erleichtern wir uns selbst und anderen Entwicklern die Arbeit. Dies steht in scharfem Gegensatz zu dem, was im Alltag oft passiert: Wenige Refactorings und viel Fokus darauf, neue Funktionen sinnvoll hinzuzufügen ohne die bisherige Struktur in Frage zu stellen.
Die Neuordnung von Code wird normalerweise dadurch angestoßen, dass man ein Code-Problem oder einen Bug wahrnimmt oder vermutet. Zum Beispiel kann eine vorliegende Methode ungewöhnlich lang sein, oder sie kann ein Beinahe-Duplikat einer anderen nahegelegenen Methode sein. Einmal erkannt, können solche Probleme behoben werden, indem der Quellcode umgestaltet oder in eine neue Form umgewandelt wird, die sich genauso verhält wie zuvor, aber stringenter funktioniert und/oder lesbarer ist.
Für eine lange Routine können eine oder mehrere kleinere Subroutinen extrahiert werden oder für doppelte Routinen kann die Duplizierung entfernt und durch eine gemeinsame Funktion ersetzt werden. Wenn das Refactoring nicht durchgeführt wird, können sich suboptimale technische Altlasten anhäufen. Auf der anderen Seite ist konsequentes Refactoring eines der wichtigsten Mittel zur Reduzierung solcher technischer Altlasten.
Zwei wesentliche Vorteile von Refactorings
Es gibt im Wesentlichen zwei allgemeine Kategorien von Vorteilen die aus der Durchführung von Refactorings entstehen:
- Wartbarkeit. Es ist einfacher, Bugs zu beheben, weil der Quellcode leicht zu lesen ist und die Absicht des Autors leicht zu verstehen ist. Dies kann erreicht werden, indem große monolithische Routinen in eine Reihe von einzelnen, prägnanten, und gut benannten Einzweckmethoden aufgeteilt werden. Dies kann, je nach Einzelfall, beispielsweise erreicht werden, indem eine Methode in eine geeignetere Klasse verschoben oder irreführende Kommentare entfernt werden.
- Erweiterbarkeit. Es ist einfacher, die Features einer Anwendung zu erweitern, wenn sie erkennbare Entwurfsmuster verwendet, und es entsteht optimalerweise eine gewisse Flexibilität, wo vorher möglicherweise keine existierte.
Automatische Unit Tests als Unterstützung
Automatische Unit Tests sollten vor dem Refactoring eingerichtet werden, um sicherzustellen, dass sich die Routinen mit dem neuen Design immer noch wie erwartet verhalten. Unit Tests können selbst großen Neuordnungen Stabilität verleihen, wenn sie mit einem einzelnen atomaren Commit durchgeführt werden. Eine gemeinsame Strategie, um sichere und atomare Refactorings zu ermöglichen, die mehrere Projekte umfassen, ist die Speicherung aller Projekte in einem einzigen Repository, das als Monorepo bekannt ist.
Die Neuordnung im Software-Design ist dann ein iterativer Zyklus, in dem eine kleine Programmtransformation durchgeführt und das Software-System anschließend auf Korrektheit überprüft wird bevor eine weitere kleine Transformation durchgeführt und getestet wird. Wenn zu irgendeinem Zeitpunkt einer der Tests fehlschlägt, wird die letzte kleine Änderung rückgängig gemacht und danach entsprechend auf andere Weise wiederholt. Durch viele kleine Schritte, jeweils gefolgt von Tests, bewegt sich das Software-System von seinem ehemals unübersichtlichen Zustand zum angestrebten Soll-Zustand. Damit dieser sehr iterative Prozess praktikabel ist, müssen die Tests entweder sehr schnell ablaufen, oder der Programmierer muss einen großen Teil seiner Zeit damit verbringen, auf den Abschluss der Tests zu warten. Befürworter von Extreme Programming und anderer agiler Herangehensweisen bei der Softwareentwicklung beschreiben diese Aktivität als einen integralen Bestandteil des Softwareentwicklungszyklus.
Techniken, die bei Refactorings zum Einsatz kommen können
Im Folgenden sind einige Beispiele für Mikro-Refactorings beschrieben – Einige davon können nur für bestimmte Sprachen oder Sprachtypen gelten. Eine längere Liste finden Sie in einschlägiger Fachliteratur und Websites. Viele Entwicklungsumgebungen bieten automatisierte Unterstützung für diese Mikro-Refactorings. Beispielsweise könnte ein Programmierer auf den Namen einer Variablen klicken und dann das Refactoring-Feld “Encapsulate field” aus einem Kontextmenü auswählen. Die IDE würde dann nach zusätzlichen Details fragen, typischerweise mit sinnvollen Voreinstellungen und einer Vorschau der Codeänderungen. Nach Bestätigung durch den Programmierer würde die Entwicklungsumgebung die erforderlichen Änderungen im gesamten Code durchführen.
Techniken, die mehr Abstraktion im Software-Design ermöglichen:
- Encapsulate Field – Kapselung von Feldern, um den Zugriff auf das Feld mit Getter – und Setter – Methoden zu erzwingen
- Generalize Type – Allgemeinere Typen erstellen, um mehr Code-Sharing zu ermöglichen
- Typprüfcode durch Status / Strategie ersetzen
- Bedingte Statements durch Polymorphie ersetzen
Techniken, um das System in ein logischeres Design zu überführen:
- Durch die Aufteilung in Komponenten wird das Software-System in wiederverwendbare semantische Einheiten zerlegt, die klare, gut definierte und ein einfach zu verwendende Schnittstellen-Designs beinhalten.
- Exktraktion von Klassen verschiebt einen Teil des Codes von einer vorhandenen Klasse in eine neue Klasse.
- Methoden extrahieren, um einen Teil einer größeren Methode in eine neue Methode umzuwandeln. Indem Quellcode in kleinere Teilen zerlegt wird, wird das System leichter verständlich. Dies gilt auch für Funktionen.
Techniken zur Verbesserung der Namensgebung und der Ordnung von Code:
- Methode verschieben oder Feld verschieben – in eine geeignetere Klasse oder Quelldatei verschieben
- Umbenennung von Feldern oder Methoden – den Namen in einen neuen ändern, der den Zweck von Codeelementen besser beschreibt
- Pull up – in eine Superklasse verschieben
- Push down – in eine Unterklasse verschieben
Regelmäßiges Code-Refactoring ist wichtig, um eine in sich stabile und performante Software erstellen und warten zu können. Trotzdem ist Refactoring einer der Schritte, der aus Ressourcengründen noch immer oftmals zu kurz kommt.