Refactoring: Strategien zum langfristigen Verbessern von Quelltextstrukturen

Mit der richtigen Herangehensweise lässt sich auch in "historisch gewachsenen" Anwendungen mit unüberwindbar scheinenden Strukturproblemen langfristig etwas für die Wart- und Erweiterbarkeit der Software tun.

In Pocket speichern vorlesen Druckansicht 4 Kommentare lesen
Refactoring: Strategien zum langfristigen Verbessern von Quelltextstrukturen
Lesezeit: 21 Min.
Von
  • Franziska Trabold
Inhaltsverzeichnis

Auch wenn die Aussicht auf Refactoring-Maßnahmen in einigen Fällen etwas einschüchterndes hat, sind sie häufig eine gute Alternative zu Entwicklungsstopp und Neuimplementierung. Sicherlich reizt es vor Beginn eines solchen Strukturierungsprojekts, die Frage nach dem Verursacher zu stellen. Sich gegenseitig die Schuld zuzuschieben hilft in derartigen Situationen allerdings nie weiter. Stattdessen sollte die oberste Direktive ("Prime Direktive") aus Retrospektiven zum Einsatz kommen: "Wir gehen davon aus, dass alle Beteiligten zu jedem Zeitpunkt nach bestem Wissen, Gewissen und Kenntnisstand gehandelt haben." [1]

Vielleicht fühlte sich das Team unter Druck, schnell zu liefern und die Qualität litt darunter. Eventuell waren die nötigen Fähigkeiten nicht vorhanden, um den Code besser auf die geschäftlichen Gegebenheiten auszurichten. Oder unklare beziehungsweise unstrukturierte Anforderungen wurden nicht genügend hinterfragt und durchdacht. Ein klärendes Gespräch mit dem ganzen Team unter bewusster Moderation kann helfen, die Vergangenheit hinter sich zu lassen. Die Diskussion sollte sich jedoch auf Lösungsansätze und Strategien konzentrieren, das Problem in Zukunft zu vermeiden.

Martin Fowler definiert Refactoring als Substantiv "Eine Veränderung in der inneren Struktur von Software (Refaktoringschritt), um sie besser verstehbar und kostengünstiger veränderbar zu machen, ohne das beobachtbare Verhalten zu verändern." und als Verb "Software restrukturieren, indem eine Reihe von Refactoringschritten durchgeführt wird, ohne das messbare Verhalten der Software zu verändern" [2].

Das Verhalten nicht zu beeinflussen ist deshalb entscheidend, weil bei jeder Änderung an ihm die Gefahr besteht, dass der Code den geschäftlichen Wert verliert. Um sicherzustellen, dass das nicht passiert, hilft es, die Funktion des Programms zu verstehen. Sie lässt sich dann vor und nach dem Refactoring vergleichen. Ein Verständnis entwickelt sich beispielsweise durch das Studium der Dokumentation. Sie ist allerdings, wenn überhaupt vorhanden, nicht immer zuverlässig. Angenommen der Code ist über einen längeren Zeitraum gewachsen und mittlerweile nicht oder nur schwer wartbar. In dem Fall ist es sehr wahrscheinlich, dass auch niemand Zeit hatte, ihn zu dokumentieren oder die existierende Dokumentation zu aktualisieren. Entwickler müssen folglich auf die Unterstützung von Experten vertrauen, die für das Produkt verantwortlich sind, es nutzen oder direkt mit Stakeholdern zusammenarbeiten. Idealerweise gibt es eine Person mit voller Entscheidungsgewalt über das Produkt, die sich im Zweifel hinzuziehen lässt. Automatisierte oder manuelle Tests helfen ebenfalls beim Verstehen.

Wichtig ist, dass die Gründe für das Restrukturieren klar sind. So ist etwa interessant, ob es sich um eine zentrale Stelle im Code handelt, die oft anzupassen ist oder Fehler enthält, oder ob es einfach darum geht, das System darauf vorzubereiten, dass eine neue Funktion eingeführt wird. Nur wenn das klar ist, lässt sich die Refactoring-Aktivität sinnvoll eingrenzen. Ansonsten landet das Team schnell in der Refaktoringhölle – vor einem unüberschaubarer Berg an Code, der sich verbessern ließe.

Falls große Abhängigkeiten bestehen, bietet sich die Mikado-Methode an, um Abhängigkeiten schrittweise anzupassen. Mit ihr lassen sich Abhängigkeiten zunächst analysieren und langsam auflösen.

public void roll(int roll) {
System.out.println(players.get(currentPlayer) + ↵
" is the current player");
System.out.println("They have rolled a " + roll);

if (inPenaltyBox[currentPlayer]) {
if (roll % 2 != 0) {
isGettingOutOfPenaltyBox = true;

System.out.println(players.get(currentPlayer) + ↵
" is getting out of the penalty box");
places[currentPlayer] = places[currentPlayer] + roll;
if (places[currentPlayer] > 11) places[currentPlayer] ↵
= places[currentPlayer] - 12;

System.out.println(players.get(currentPlayer)
+ "'s new location is "
+ places[currentPlayer]);
System.out.println("The category is " + ↵
currentCategory());
askQuestion();
} else {
System.out.println(players.get(currentPlayer) +
" is not getting out of the penalty box");
isGettingOutOfPenaltyBox = false;
}

} else {

places[currentPlayer] = places[currentPlayer] + roll;
if (places[currentPlayer] > 11) places[currentPlayer] =
places[currentPlayer] - 12;

System.out.println(players.get(currentPlayer)
+ "'s new location is "
+ places[currentPlayer]);
System.out.println("The category is " + currentCategory());
askQuestion();
}
}

Das J.B. Rainsburgers Legacy Code Retreat entliehene obige Beispiel enthält eine Java-Methode, die schwer zu lesen und schwer zu warten ist. Sie wurde als Quelle mehrerer Bugs identifiziert und ist für den Großteil des Geschäftswerts des Systems verantwortlich.

Mehr Infos

Sonderheft "Altlasten im Griff"

Mehr Artikel zum Thema Legacy-Code sind im Sonderheft iX Developer 01/2017 zu finden, dass sich unter anderem im heise Shop erwerben lässt.

Angenommen, das ursprüngliche Entwicklungsteam ist nicht mehr in der Firma. Und die letzte Person, die für das Produkt verantwortlich war, hat kaum Zeit, Fragen zu beantworten. Kein Mensch hat sich darum gekümmert automatisierte Tests vorzulegen. Zwar könnte das Team selbst welche hinzufügen, um sicherzustellen, dass der Funktionsumfang intakt bleibt.

Neue automatisierte Tests sind allerdings oft erst möglich, wenn kleine Änderungen am Code ihnen den Weg gebahnt haben. Sie wiederum bergen immer ein gewisses Risiko, das Verhalten des Codes zu verändern, was nicht im Sinne der Refactoring-Definition ist. Um sich also einen ersten Überblick darüber zu verschaffen, was die Aufgabe des betrachteten Programms ist, hilft es, es auszuführen. Das sollte in einer sicheren Umgebung passieren, wo das manuelle Testen keinen Schaden anrichten kann. Wenn das Benutzen der Produktionsumgebung eines Onlineshops etwa echte Bestellungen auslöst, ist es wohl besser, das Programm lokal auszuführen. Im Beispiel handelt es sich um ein einfaches Konsolenprogramm, die Technik lässt sich jedoch auf komplexere Systeme übertragen.

Die Ausgabe sieht wie folgt aus:

Rock Question 3
Answer was correct!!!!
Sue now has 4 Gold Coins.
Chet is the current player
They have rolled a 1
Chet's new location is 4
The category is Pop
Pop Question 1
Answer was corrent!!!!
Chet now has 6 Gold Coins.

Process finished with exit code 0

Die Ausgabe lässt erkennen, dass es im Programm um Spielende, Kategorien und Würfelwürfe geht. Nutzer können Münzen sammeln und Fragen beantworten.

Außerdem enthält die Ausgabe eine Abweichung: mal heißt es "corrent", mal "correct". Der erste Impuls mag nun sein, dies zu vereinheitlichen. Das ist jedoch nicht Teil des Refactoring-Prozesses und ist daher auch nicht zu vermischen – selbst wenn der Fehler offensichtlich erscheint. Vielleicht sind andere Systeme oder Nutzer bereits von dieser Abweichung abhängig oder die Änderung birgt ein anderweitiges Risiko. Derartige Maßnahmen sind also immer mit der Person in Produktverantwortung abzustimmen.