Wie du mit diesen 8 einfachen Schritten deinen Quellcode perfekt refaktorisierst

Autor: Sergej Kukshaus Veröffentlicht: vor einem monat Lesedauer: ca. 10 Minuten

Wer kennt nicht das Problem?

Eine Software wächst und wächst über die Jahre und es entsteht immer mehr Quellcode.

Kollegen kommen und gehen. Immer mehr und mehr Entwickler arbeiten an diesem, einem Projekt. Das Problem jedoch: Es arbeitet immer nur ein paar Kollegen gleichzeitig an dem Projekt.

Nicht selten, entsteht bei solch einer Art von Software einfach nur Frust. Frust nicht weiterzukommen. Immer auf einer Stelle zu treten.

Schlimmer noch. Man fühlt sich für die neu entdeckten Fehler verantwortlich. Aber auch für Fehler, die neu entstanden sind. Und dies an Stellen, an denen man gar nichts „angefasst“ hat.

Ändert man eine Zeile Code, kann dies natürlich auch Implikationen auf andere Codestellen haben.

Meine Lieblingsmetapher hierfür ist: Man möchte in der Wand ein Nagel für ein neues Bild einschlagen und dabei fällt das Dach vom Haus runter.

Jeder weiß es, keiner unternimmt etwas.

Das Problem bei solcher Software ist immer dass selbe:

  • Jeder weiß, dass die Software unwartbar ist.
  • Durch die vielen Entwickler und uneinheitlichen Stil, unterscheiden sich die Programmierstile sogar teilweise innerhalb einer Methode.
  • Keiner möchte der Sündenbock sein, wenn etwas nicht mehr funktioniert.
  • Das Management möchte nicht unnötige Ressourcen investieren, nur damit der Quellcode wartbarer wird.
    • Wozu auch? Code-Wartbarkeit lässt sich nicht verkaufen. Oder hast du schon in irgendwelchen Release-Notes gelesen: „Keine neuen Features, aber der Code ist jetzt schöner!“?
  • Neue Anforderungen werden (nicht selten) unter Zeitdruck implementiert.
    • Denn wenn uns die liebe Zeit fehlt, machen wir schneller Fehler.
  • Wenn etwas nicht funktioniert, baut man etwas drum herum damit es funktioniert. Die Fehlerursache bleibt jedoch erhalten und wird nicht behoben.
    • Das ist so, als ob man versuchen würde Hustensaft bei einer Lungenentzündung zu nehmen. Lindert den Schmerz, Ursache wird nicht bekämpft.

Mein Azubi meinte einmal ganz treffend:

Das Entwickeln von [Tool X] ist so, als ob man Minesweeper ohne Zahlen spielt würde.

Eine 1000 Kilometer weite Reise beginnt mit dem ersten Schritt.

Das sagte bereits der chinesische Philosoph Lao-Tzu vor über 2600 Jahren.

Ich habe auch oft gehört: „Das ist so kaputt, daran kann man nichts mehr retten“.

Lass mich dir eine kleine Geschichte erzählen:

Bei mir in der Nachbarschaft steht ein Haus von 1846 (kein Scheiß).

Das Haus stand Jahrelang leer und unbewohnt. Dementsprechend war es auch heruntergekommen.

Einfach abreißen konnte man es auch nicht. Den es ist eine Doppelhaushälfte und steht unter Denkmalschutz.

Es war wirklich eine Bruchbude. Und selbst das ist noch eine Beleidigung für andere Bruchbuden. Das an diesem Haus noch keine „Lost-Place-Jäger“ auftauchten, grenzt an Wunder. Dafür wiederum war es nicht interessant genug.

Vor gut einem Jahr hat dann endlich ein sehr fleißiger Mann (inkl. Familie) dieses Haus für „günstig Geld“ erworben. Innerhalb von nur 3 Monaten war das Haus bereits bewohnbar.

Nach weiteren drei Monaten war die Außenanlage (Garten, Fassade, etc.) nicht mehr wiederzuerkennen.

Der Mann hat innerhalb von nur einem Jahr den Wert des Hauses vervierfacht. Und das alles am Wochenende und nach Feierabend.

Wir Entwickler neigen viel zu Schnell dazu, Dinge einfach wegzuwerfen und neu zu entwickeln.

Doch ein Code-Rewrite birgt viele Gefahren. Nicht wenige Unternehmen sind an einem Rewrite bereits Pleite gegangen. Oder kennst du Unternehmen wie Lotus oder Netscape?

Gerade an dem Beispiel von meinem (neuen) Nachbarn, kann man sehen, dass man selbst aus einem hoffnungslosen Fall etwas Gutes machen kann.

Ebenfalls kann man davon auch Ableiten, dass es nicht irgendwer machen soll, sondern der Beste!

Schritt 1: Code aufräumen

Code ist für mich an dieser Stelle alles, was in einer einzigen Zeile steht.

Mit anderen Worten, man nimmt sich eine einzige Methode und schreibt diese so lange um, bis in einer Zeile nur eine einzige Anweisung steht.

Was da steht, ist im Grunde genommen Wurscht.

  • Blödes Naming? –> Umbenennen
  • Mehr als nur ein Funktionsaufruf? –> In mehrere Zeilen aufteilen
  • Komplexe Bedingungen? -> In mehrere Zeilen aufteilen
  • Verwendung von „var“? –> Spezifischeren Datentypen wählen
  • usw.

Wichtig ist, der Code muss hinterher theoretisch gut lesbar sein.

Dabei gilt es noch nicht einmal um das Verständnis. Ein jeder muss sofort auf den ersten Blick erkennen, was diese eine Zeile genau macht.

Hier ein paar Beispiele:

// Beispiel 1:
// schlecht
var cnt = pernsonList.Count();
// besser
int numberOfPersons = persons.Count();

// Beispiel 2:
// sehr schlecht
var range = $"{personList.Where(p => p.Age >= 18).Age.Min()} - {personList.Where(p => p.Age >= 18).Age.Max()}";
// besser
Person[] adults = persons.Where(p => p.Age >= 18).ToArray();
int youngest = adults.Min();
int oldest = adults.Max();
string ageRange = $"{youngest} - {oldest}";

// Beispiel 3:
// schlecht
if (x >= 42 && x <= 50 && y <= 17 || y > 18 && z >= 42 && z <= 50)
	return false;
else
	return true;

// besser
const int lowerBound = 42;
const int upperBound = 50;
bool isXWithinRange = x >= lowerBound && x <= upperBound;
bool isZWithinRange = z >= lowerBound && z <= upperBound;
bool isYRelevant = y <= 18;

return isYRelevant ? isXWithinRange : isYRelevant;

Na? Wer findet den Bug in Beispiel 3 vor dem refactoring? Und warum ist es gleich aus mehreren Gründen nicht empfehlenswert sowas zu machen?

Schreib es doch unten in die Kommentare 🙂

Schritt 2: Methoden aufräumen

Nachdem der erste Schritt erledigt ist, haben wir mehr Code als vorher.

Toll!

Jedoch benötigen wir diese Basis für das weitere Vorgehen. Nicht zwangsläufig, aber es hilft ungemein.

In den meisten Fällen sieht der Code hinterher so (oder so ähnlich) aus

Refactoring: Starte mit Legacy Code

Innerhalb einer Methode passiert viel zu viel. Diese Methode hat gleich vier Aufgaben, um die es sich kümmern gilt.

Jetzt musst du zunächst versuchen, diese Aufgaben zu identifizieren und so nah aneinander bringen wie es nur geht. Die größte Herausforderung hierbei besteht darin, nicht die bestehende Logik zu beschädigen.

Dabei kann es sehr schnell passieren, dass das Vertauschen zweier Zeilen eine große Auswirkung hat.

Ist dies nicht möglich, so entsteht hier eine neue Aufgabe.

Der Quellcode sollte hinterher so (oder so ähnlich) aussehen:

Refaktorisierung: Eine Methode strukturieren

Wir haben saubere Code-Blöcke. Man erkennt (fast) sofort die Aufgaben der Methoden.

Schritt 3: Code auslagern

Doch Halt!

Wenn man doch eine Aufgabe sieht, warum gibt man dieser Aufgabe nicht gleich einen passenden Namen?

Bingo, Ingo 🙂

Doch fang bitte nicht an, den jeweiligen Code-Blöcken irgendwelche kommentare zu verpassen.

Stattdessen definiere die Aufgabe als eigene Methoden und lagere diese entsprechend aus.

Refactoring: Quellcode in andere Methoden auslagern

Dabei muss man auch hier auf eine besonders gute Namensgebung acht legen. Benennst du jetzt die Methode schlecht, war die Arbeit, nämlich für die Katz.

Das gute dabei ist, Legacy-Code wurde bzw. wird häufig per Copy&Paste geschrieben. Die neuen Methoden kann man an geeigneter Stelle also jetzt schon wiederverwenden 🙂

Natürlich dürfen die neuen Methoden nicht zu lang werden. Falls das geschieht, dann macht diese Methode ebenfalls zu viel und kann weiter unterteilt werden (zurück zu Schritt 1).

Eine gute Kenngröße für eine optimale Methode ist: maximal 20 loc (lines of code) oder einer Komplexität von 5. Je nachdem, was früher erreicht ist. Generell gilt: je kleiner die Methode, desto besser.

Wenn wir damit fertig sind, sieht unser Quellcode in etwa so aus:

Refaktorisierung: Nur noch ausgelagerte Methoden verwenden

Schon viel besser, nicht war?

Schritt 4: Zusammenhängende Methoden erkennen

Sobald alle Methoden sauber verarbeitet sind, folgt schon der nächste Schritt.

Ich muss dich jedoch vorwarnen. Händisch ist es eine Qual das durchzuführen.

Glücklicherweise gibt es in Visual Studio (leider nur ab Enterprise) eine Funktionalität die dir genau diese Arbeit abnimmt und auf ein Minimum reduziert.

David Tielke bezeichnet das als die „Dressierte-Affen-Methode“. Damit möchte er die Einfachheit der Methode unterstreichen (sobald man die notwendigen Tools dafür hat).

Bei dieser Methode gehst du wie folgt vor:

Schritt 1:

Du schreibst alle privaten Felder (manchmal auch als Instanzvariable bezeichnet) versetzt in der Mitte eines Diagramms auf.

Die Methoden werden dann verteilt daneben aufgezeichnet. Am Ende sollte es in etwa so aussehen:

Refaktoring: Methoden und Felder aufzeichnen

Die gelben Kreise stehen hier für Methoden und die lila Kreise für die jeweiligen Felder.

Schritt 2:

Sobald ein Zugriff zwischen einem Feld und einer Methode oder zwischen den Methoden besteht einfach einen Pfeil zeichnen.

Refaktoring: Abhängigkeiten Identifiezieren

Dann verschiebst du die gelben Kreise, in die nähe der Lila Kreise.

Die lila Kreise werden nicht mehr angepackt. Nur noch die Gelben. Im Grunde genommen musst du versuchen die roten Pfeile möglichst kurzzuhalten.

Mit anderen Worten, versuchen zu „Clustern“. Alles, was zusammen gehört, wird auch zusammen gebracht.

Falls du keine Enterprise Version hast, kannst du das auch mit yEd oder auch draw.io machen. Leider werden die dabei die Kreise nicht vorgeneriert.

Schritt 5: Methoden auslagern

Auf dem Diagramm kannst du jetzt sehr gut die Abhängigkeiten und Zusammenhänge erkennen.

Immer da, wo nur eine (maximal zwei) Abhängigkeit besteht, ziehst du ein Strich durch das Diagramm.

Das ist ein potenzieller Kandidat für eine eigenständige Klasse.

Refaktorisierung: Methoden clustern und extrahieren

Wenn du jetzt auf die Methodennamen schaust, kannst du relativ einfach daraus Klassennamen abstrahieren.

Im oberen Fall wäre C2 unsere Hauptklasse. Sowohl C1 als auch C3 sind ab jetzt komplett eigenständig und haben keine Abhängigkeit mehr zu den anderen beiden Klassen.

Versuche in diesem Schritt so viele Klassen wie nur möglich zu erstellen. Und halte die Klassen so klein wie nur möglich.

Der YouTuber und Consultant Tim Corey geht sogar so weit und sagt, dass eine Klasse zu lang ist, wenn diese nicht mehr auf einen Monitor passt.

Schritt 6: Klassen strukturieren

An dieser Stelle müssten die Anzahl der Klassen exorbitant gestiegen sein.

Zehn- oder Zwanzigmal mehr Klassen sind keine Seltenheit. Je nachdem wie gut du hier arbeitest und wie schlimm die Codebasis vorher war.

Und wiederum schaust du auf die Namen der Klassen (die hast du ja jetzt vernünftig gewählt).

Alles, was irgendwie miteinander zu tun hat, kommt in einen separaten Ordner.

Dabei musst du entscheiden, was dieses was überhaupt ist.

Allerdings darfst du nicht Geschäftslogik, Datenstruktur und Benutzeroberfläche miteinander vermengen.

Schritt 7: Komponenten erstellen

Du bist jetzt an dem Schritt angekommen, an dem du dein ganzes Projekt neu strukturiert hast.

Herzlichen Glückwunsch 🙂

Was liegt jetzt näher, als die einzelnen Ordner in eigene Pakete auszulagern?

Das einzige worauf man achten muss ist, dass keine Kreisverweise entstehen. Das heißt, dass du nicht auf einmal in dem Model eine Abhängigkeit auf das Repository hast.

Schritt 8: Unit-Tests schreiben

Unit-Tests können nur effektiv geschrieben werden, wenn Methoden und Klassen schön klein gehalten werden.

Auch die Anzahl der Abhängigkeiten sollte so gering wie möglich sein.

Eins sollte jedoch klar sein. Unit-Tests nach der Entwicklung zu schreiben ist fast schon schwieriger als die Entwicklung selbst.

Bis man eine vernünftige Abdeckung erreicht, muss man Zeit investieren. Viel Zeit. Sehr viel Zeit.

Ich würde dir Raten, dich auf die Problemfälle zu konzentrieren.

Mit der Zeit kommen dann immer mehr und mehr Tests hinzu. Und je mehr Tests du im Projekt hast, desto weniger Zeit benötigst du später noch für manuellen Tests.

Und glaube mir, jede Software hat ihre Problemfälle.

Es ist immer die eine fragile Funktion, die Anfällig für Fehler ist. Genau diese Funktion kann man durch automatisierte Tests abdecken.

ACHTUNG: Große Stolperfalle

Während des Refactorings musst du höllisch darauf aufpassen, keine Abhängigkeiten zu verletzten.

Wenn du eine zentrale Komponente Refaktorisieren möchtest, darfst du auf gar keinen Fall irgendwas ändern was nicht als privat gekennzeichnet ist.

Du darfst selbstverständlich irgendwas neue hinzufügen. Methoden umbenennen oder gar löschen ist jedoch Tabu.

Auch, wenn da gerade völliger Nonsens steht. Darfst du das nicht einfach mal eben umbenennen. Die Namespaces werden nicht angepasst.

Wenn das doch mal sein muss, dann kommuniziere das an alle, die diese Komponente verwenden.

Unter C# gibt es das obsolte Attribut. Verwende es und warne den Benutzer. Du kannst damit sogar soweit gehen und sagen, dass der Quellcode nicht mehr kompilierfähig sein soll. Gib dem Benutzer genügend Zeit seine Komponenten umzustellen. Anschließend kannst du die Methoden oder Klassen löschen.

Aus eigener Erfahrung kann ich sagen, dass es nichts Ärgerliches gibt als nicht funktionierender Quellcode, wenn man als Nutzer neue Pakete heruntergeladen hat.

Dabei ist nicht kompilierbarer Code noch das geringste Übel. Schlimmer ist es, wenn auf einmal Funktionen innerhalb einer Komponente ganz anders funktionieren. Dies fällt nämlich erst zur Laufzeit auf.

Meist wird solch eine Komponente in Projekten verwendet, die ebenfalls eine schlechte Testabdeckung haben und somit auf gute und manuelle Tests angewiesen ist.

Fazit: Refactoring

Eine 1000 Kilometer weite Reise beginnt mit dem ersten Schritt.

Lao-Tzu

Wenn es um das Thema Refactoring geht, sollte eins klar sein. Je nach Umfang des Projekts ist es nicht „mal eben“ erledigt. Es dauert mehrere Wochen oder gar Monate um einen sauberen Stand zu bekommen.

Du musst dich aber auch zwischenzeitig Zwingen, dein Weg zu verfolgen.

Auch beim Refactoring ist das allerwichtigste: Sei konsequent. Entscheide dich für einen Weg und verfolge diesen auch. Sonst passiert am Ende sowas hier:

Was sind deine Erfahrungen mit Refactoring? Musstest du bereits bestehenden Quellcode Refaktorisiert? Wenn ja, wie bist du dabei vorgegangen und wie war hinterher das Endergebnis? Hat sich der Aufwand im Endeffekt im Vergleich zum Nutzen gelohnt?

Sei der erste und teile deine Meinung mit der Welt!
... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.