Wenn Sie neu in eine Codebasis einsteigen, stellen Sie vielleicht fest, dass sie zwar für Sie neu ist, aber nicht für die Welt. Es handelt sich um Code, den es schon seit Ewigkeiten gibt und der schwer zu ändern und zu pflegen ist. Dies ist per Definition Legacy-Code, und es ist Ihre Aufgabe, damit zu arbeiten. Selbst wenn es den Code noch nicht lange gibt oder er zu Beginn des Projekts perfekt war, wird er mit der Zeit immer schlechter. Ich sehe das wie den zweiten Hauptsatz der Thermodynamik, der besagt, dass die Entropie eines isolierten Systems im Laufe der Zeit nicht abnimmt, sondern sich zu einem Gleichgewicht mit maximaler Entropie entwickelt.
Die Entropie - oder Unordnung - von großen, schlecht getesteten Klassen, die wir ändern sollen. In diesem Beitrag erkläre ich, wie ich an eine solche Codebasis herangehe. Ist sie hoffnungslos? Wenn nicht, wo fangen wir an, sie aufzuräumen?
Metriken
Ich arbeite als Softwareberater und als Teil meiner Arbeit tauche ich in neue Codebases ein. Daher finde ich es nützlich, einen schnellen Überblick über den Zustand einer Codebase zu erhalten. Eine nützliche Kennzahl ist die Codeabdeckung, d.h. der Prozentsatz des Codes, der durch Tests, z.B. Unit-Tests, "abgedeckt" ist. Ein Unit-Test gibt mir die Gewissheit, dass ich, wenn ich einige Codezeilen ändere, eine Rückmeldung darüber erhalte, ob ich die Funktionalität des Codes beschädigt habe. Er dient auch als Dokumentation, da er die Absicht der Person erklären kann, die ihn erstellt hat.
Eine andere Sache, die ich mir ansehe, ist die zyklomatische Codekomplexität von Klassen. Dies ist ein Maß für die Anzahl der Ausführungspfade durch einen Teil des Codes, zum Beispiel eine Methode. Für jeden Ausführungspfad wäre ein Test erforderlich, um eine 100%ige Codeabdeckung zu erreichen (dieser Grad der Codeabdeckung ist nicht immer erreichbar, da es immer Teile des Codes gibt, die sich nicht testen lassen). Abdeckungsgrad und Komplexität sind oft miteinander verbunden. Wenn die Komplexität einer Klasse zu hoch ist (z.B. über 10-15), enthält sie zu viel Logik. Vielleicht hat sie mehr als eine Verantwortung (sie verstößt gegen das Prinzip der einzigen Verantwortung). Sie ist dann schwer zu testen und schwer zu pflegen. Eine schwer zu wartende Codebasis ist per Definition eine Legacy-Codebasis.
Natürlich sind die Codeabdeckung und die zyklomatische Komplexität des Codes nicht die einzigen Maßstäbe für die Qualität des Codes, aber ich halte sie für einen guten Ausgangspunkt, weil sie ein so gutes Maß für die Wartbarkeit sind. Wenn etwas wartbar ist, ist es viel einfacher, es zu ändern und weiterzuentwickeln. Und alles, was wir wollen, ist, dass es vorwärts geht, denn wir wollen neue Funktionen!
Sie könnten sich nur die Codeabdeckung und die Komplexität ansehen und die Klassen identifizieren, die verbessert werden müssen, und mit der Bereinigung beginnen. Teilen Sie Klassen auf, fügen Sie Tests hinzu, refaktorisieren Sie, vereinfachen Sie. Die Codeabdeckung wird zunehmen, die Komplexität wird abnehmen, die Wartbarkeit wird zunehmen. Das ist alles gut, aber in der Zwischenzeit werden Sie Ihren Arbeitstag erschöpft beenden und Ihr Vorgesetzter könnte sich fragen, was Sie gemacht haben und ob die Zeit gut investiert war. Wahrscheinlich nicht. Dieser Ansatz erfordert (je nach Größe Ihrer Codebasis) einen enormen Aufwand, und nicht alles davon ist notwendig.
An dieser Stelle kommt der Code Churn ins Spiel. Churn ist ein Maß dafür, wie oft sich ein Teil Ihrer Codebasis im Laufe der Zeit ändert. Wenn Sie dieses Maß mit der Komplexität und der Codeabdeckung kombinieren, können Sie erkennen, wo die problematischen Teile Ihrer Codebasis liegen, die sich ebenfalls häufig ändern und daher von einer Bereinigung profitieren würden. Denn anders als bei einem Finanzkredit werden die Zinsen für Ihre technischen Schulden (für Ihren Legacy-Code) gezahlt, wenn Sie Ihren Code ändern, und nicht im Laufe der Zeit. Ich gehe hier davon aus, dass ein Teil der Codebasis, der kürzlich geändert wurde, wahrscheinlich auch in naher Zukunft geändert wird (was ich für fair halte).
Abwanderung vs. zyklomatische Code-Komplexität
Eine hilfreiche Methode, um dies zu verdeutlichen, besteht darin, die Codekomplexität gegen die Abwanderung aufzutragen, wie im folgenden Beispiel.

Jeder Punkt im Diagramm steht für eine Klasse, rote Punkte haben eine Codeabdeckung < von 75%. Der Churn ist die Anzahl der Änderungen an einer Klasse in den letzten 12 Monaten.
Eine gute Möglichkeit, dieses Diagramm zu betrachten, ist, es in vier Quadranten aufzuteilen:
- Unten links: Dies ist der Bereich, in dem sich der Code nicht oft ändert und die Komplexität gering ist. Hier gibt es keine Probleme!
- Unten rechts: ebenfalls in der unteren (und guten) Hälfte der Karte, aber dieser Teil ändert sich häufig. Das ist in Ordnung, die Komplexität ist gering! Hier könnten einige Datenklassen stehen, zum Beispiel die Konfiguration.
- Oben links: Hier ist der Code komplex, aber er ändert sich nicht oft. Sie könnten Ihren Code hier verbessern, aber Sie werden nicht viele der Vorteile nutzen können, da Sie diese bei der nächsten Änderung erhalten würden.
- Oben rechts: der Hauptbereich, in dem sich die Dinge oft ändern und der Code komplex ist. Wahrscheinlich braucht man hier mehr Zeit als irgendwo sonst im Code, um etwas zu ändern. Hier müssen Schulden getilgt werden!
Beachten Sie auch, dass ich das Diagramm auf der logarithmischen Achse zeichnen musste, um einen vernünftigen Überblick über den Zustand dieser Codebasis zu erhalten.
Wie Sie mit der Verbesserung einer Legacy-Codebasis beginnen
Da es nun an der Zeit ist, Ihre Codebasis zu verbessern, finden Sie hier einige Tipps, die sich für mich bewährt haben.
- Wenn Sie sich ernsthaft mit dem Refactoring eines Teils Ihrer Codebasis befassen, sollten Sie sich auf den oberen rechten Quadranten konzentrieren. Dort werden Sie in Zukunft den größten Nutzen daraus ziehen. Das bedeutet nicht, dass Sie den Code im oberen rechten Quadranten einmalig bereinigen müssen. Das wird anstrengend sein, und danach wird es langsam wieder in Richtung Legacy gehen. Was wir brauchen, ist eine Änderung der Mentalität, der Disziplin, um sicherzustellen, dass Sie nicht wieder in eine solche Situation geraten.
- Machen Sie dies zu einem Teil Ihrer täglichen Arbeit bei der Erstellung von Funktionen. Wann immer Sie einen Teil des Codes anfassen, versuchen Sie, die Komplexität niedrig und die Codeabdeckung hoch zu halten. Machen Sie aus der Pfadfinderregel, den Lagerplatz sauberer zu verlassen, als Sie ihn vorgefunden haben, eine Routine.
- Für jede Funktion, die Sie hinzufügen, empfehle ich Ihnen, den Ansatz der testgetriebenen Entwicklung (TDD) zu befolgen, damit Sie nicht von vornherein zu viel Komplexität und schlecht getesteten Code erzeugen.
- Wenn der Code wirklich in einem Zustand ist, in dem Sie nicht wissen, wie Sie ihn verbessern oder richtig testen können, empfehle ich Ihnen das Buch Working Effectively with Legacy Code von Micheal Feathers.
Ich habe diesen Ansatz bei einem meiner Projekte verfolgt. Unten sehen Sie die Churn vs. Complexity Diagramme der geänderten Dateien in einer Pull-Anfrage, bevor (links) und nachdem (rechts) sie geändert wurden:

Die Codeabdeckung war hier zwar nicht allzu schlecht, aber die Komplexität schon. Wie Sie sehen können, hat sich die Gesamtkomplexität verringert, indem die komplexeren und häufig geänderten Klassen (die beiden rechts) in eine Reihe neuer Klassen mit geringer Komplexität und hoher Testabdeckung aufgeteilt wurden. Da diese Klassen so neu sind, werden sie wahrscheinlich bald nach rechts wandern, denn es kann ein wenig dauern, bis sie ein ausgereifteres Design erreichen. Das ist in Ordnung, solange sich die Komplexität in Grenzen hält.
Beachten Sie, dass wir hier noch nicht alles gelöst haben. Einige Klassen befinden sich noch in der oberen Hälfte der Tabelle. Das ist auch in Ordnung, denn solange sie bei jeder Änderung nach unten wandern, werden wir sie schließlich an der richtigen Stelle finden.
Wie erstellt man diese Tabellen?
Die Erstellung dieser Diagramme erfordert einige Schritte, ist aber recht einfach. Zunächst müssen Sie die Metriken abrufen. Für die Codeabdeckung und die Komplexität habe ich eine sonarqube-Analyse durchgeführt und deren API verwendet, um die Metriken abzurufen. Damit die Codeabdeckung in sonarqube verfügbar ist, habe ich Abdeckungsberichte aus Unit-Tests mit jacoco erstellt (das von mir analysierte Projekt war in Java geschrieben). Die Churn-Daten habe ich von Git übernommen, aber jedes SCM sollte Ihnen diese Zahlen liefern können. Für jede Datei habe ich etwas in der Art wie folgt ausgeführt:
git log --oneline --since=12.month some-file | wc -lDie Daten wurden mit Google Visualization in ein Diagramm eingefügt. Das war's schon!
Zusammenfassung
In diesem Beitrag habe ich Ihnen gezeigt, wie Sie anhand von Codekomplexität und Abwanderung herausfinden können, wo Sie mit der Bereinigung Ihrer Legacy-Codebasis beginnen und sich darauf konzentrieren sollten. Vielleicht fühlen Sie sich hoffnungslos, wenn Sie mit der Erstellung eines Features in einer Legacy-Codebasis konfrontiert werden, aber keine Angst! Wenn Sie versuchen, große, einmalige Aufräumarbeiten zu vermeiden und stattdessen schrittweise aufzuräumen, indem Sie die Pfadfinderregel anwenden, sich insbesondere auf den oberen rechten Bereich des Diagramms von Churn und Codekomplexität konzentrieren und der TDD-Praxis folgen, werden Sie es schaffen.
Viel Glück!
Verfasst von
Simeon van der Steen
Unsere Ideen
Weitere Blogs
Contact
Let’s discuss how we can support your journey.



