Boy Scout Principle
Motivation
Zu den Regeln der Pfadfinderbewegung gehört der Satz:
Always leave the camp ground cleaner than you found it.
oder wie der Vater der Bewegung – Robert Baden Powell – es ausgedrückt hat:
Try and leave this world a little better than you found it.
In der Realität herrscht die gegenteilige Praxis vor (oder eben die gleiche Praxis mit negativem Vorzeichen): Liegt irgendwo Müll herum, dann scheint das die Menschen dazu zu animieren dem herumliegenden Müll weiteren Müll hinzuzufügen. Dieses Phänomen wurde in den 80er Jahren als "Broken Window Effect" beschrieben. Aber was hat das nun mit Software Engineering zu tun? Zum Äquivalent des Broken Windows Effect im Software Engineering ist hier recht nett beschrieben (da ist wohl ein Link verloren gegangen). Um dessen Vermeidung und die Frage was man statt dessen für die Software-Qualität tun kann soll es im Folgenden gehen.
Über das Problem
Wer lange genug programmiert hat, weiß wie schlimm schlechter Code aussehen kann. Und immer wenn man denkt man hat schon alles gesehen tritt man in eine Java-Klasse die alles dagewesene in den Schatten stellt. Der erste Reflex ist meistens "weg damit, alles neu machen". Man bezeichnet dieses Refactoring-Vorgehen auch mit dem treffenden Namen "Big Bang". Die Umsetzung bereitet in der Praxis allerdings eine ganze Reihe von Problemen. Das größte ist, daß eine umfassende Refactoring-Maßnahme Aufwände generiert die aus keinem Budget zu bekommen sind. Das liegt nicht nur an knausrigen Budget-Verwaltern, das liegt auch in der Natur der Entstehung solcher Code-Höllen. Wer sich beim Codieren nicht um die Qualität kümmert schafft technologische Schulden, für die keine Deckung existiert und für die in der Regel keine Rücklagen gebildet werden. So kommt Müll zu Müll und aus kleinen dreckigen Code-Ecken werden langsam aber sicher riesige Müllhalden. Ein anderes Problem verbirgt sich hinter der Metapher vom Abreißen und Neuaufbau des Systems. Diese aus dem Hausbau stammende Metapher läßt sich nicht eins zu eins auf die Software-Entwicklung übertragen. Denn das neue System wird ja nicht einfach aus "neuem" Material gebaut, sondern soll die Substanz des abgerissenen Systems wiederverwerten. Oder auf den Code bezogen: Die Business-Logik, die sich im Müllhaufen verbirgt – und das ist ja das eigentliche Kapital der Software, das was das System wertvoll macht – muß erhalten bleiben. Das funktioniert – vielleicht – wenn man eine exzellente und erschöpfende Dokumentation des Systems hat die genau die im System enthaltene Fachlichkeit dokumentiert, aber zu welchem System hat man eine solche Dokumentation?
Der einzige Weg ist dann das akribsche Analysieren des bestehenden Codes und das Übertragen in die neue Anwendung. Und da es sich um eine vollständige Neuentwicklung handelt muß jede Zeile im Rahmen der Neuentwicklung untersucht werden; dazu gehört veralteter, nicht mehr verwendeter Code ebenso wie Code- Gewölle deren Analyse aufgrund der Komplizierteit sehr zeitaufwändig ist. Es gibt keine Möglichkeit, auf irgendeinen Teil dieser Analyse zu verzichten, denn mit der Migration muß sämtliche Funktionalität übertragen werden. Sei sie nun offensichtlich oder unter hunderten verschachtelter Klassen vergraben. Damit ist in vielen Fällen das Migrationsprojekt mangels Budget gestorben. Und weil die Komplettlösung nicht umsetzbar ist, passiert meistens gar nichts. Dabei muß noch nicht einmal die gesamte Anwendung betroffen sein, Oft geht es nur darum wesentliche Teile einem Refactoring zu unterziehen, was dann – Budget! Budget! – ebenfalls nicht geschieht.
Über die Lösung
Wenn also der Big-Bang-Ansatz nicht durchführbar ist, was dann? Die kleine, mühsamere und weniger beglückende Lösung ist der Weg der kleine Schritte. Und wenn man vor lauter Chaos nicht weiß wo anfangen, fängt man am besten da an wo man gerade steht: Wann immer der Entwickler ein Stück Code bearbeitet oder reviewt – und sei es noch so klein – dann habe er dabei immer die Frage im Hinterkopf
- Was kann ich hier ohne viel Aufwand besser machen?
Das scheint erstmal für lange Zeit gar nichts zu bringen und es ersetzt auch nicht das kostenintensive Refactoring größerer Programmteile – besonders, wenn die Architektur Probleme verursacht – aber es hilft auf lange Sicht Aufwände zu senken und kann den Code so vorbereiten, daß große Refactoring-Maßnahmen überhaupt erst möglich werden.
Was kann man konkret tun?
Erlaubt ist alles, was die Code-Qualität verbessert, dabei aber die Funktionalität nicht beeinträchtigt und solange man die Schnittstellen nicht anfaßt, ist praktisch alles erlaubt.
- Unit-Tests schreiben
- Dokumentation schreiben
- Kommentare korrigieren und ergänzen
- Methoden- und Klassennamen umbenennen
- Toten und auskommentierten Code entfernen
- Literale durch Konstanten und Konstanten durch Enums ersetzen
- Zweifelhafte Stellen als TODO oder FIXME markieren
- Ungenutzte Teile als "deprecated" markieren
- Code-Blöcke in Methoden auslagern
- Mehrfach verwendete Blöcke in Klassen auslagern
- if-Bedingungen umformen
- ...
Worauf sollte man achten?
Entscheidend ist, daß durch das Refactoring nichts kaputt geht. Bevor man also anfängt den Code umzukrempeln muß man sich darüber klar werden, welche Auswirkung die Änderung hat. Wenn kein eigenes und ausreichendes Budget für die Überarbeitung zur Verfügung steht, gestaltet man die Änderungen so defensiv wie möglich. Am besten sichert man die Änderungen immer durch automatische Unit-Tests ab.
Aus diesem Grunde stehen die Unit-Tests in der Aufzählung oben auch ganz oben. Jeder Entwickler sollte immer alles durch Unit-Tests abdecken – so die Theorie. Die Erhöhung der Testabdeckung ändert zwar überhaupt nichts an der Code-Qualität, ist aber trotzdem sehr wertvoll. Tests sichern den Code gegen Änderungen ab. Bei der nächsten Änderung, Erweiterung oder dem nächsten Refactoring merkt man hoffentlich sofort, wenn etwas kaputt gegangen ist.
Nicht umsonst antwortet Michael Feathers auf die Frage "Was ist Legacy Code?" ganz lapidar
To me, legacy code is simply code without tests
Noch was?
Der brave Pfadfinder kümmert sich nicht darum, wer nach ihm kommt. Er sorgt einfach dafür, daß sich der Nächste wohler fühlt wenn er sein Zelt aufschlägt. Wenn er aber weiß, daß demnächst eine große Gruppe zum Camping anrückt, dann kann er – mit seinen Genossen – schon mal vorarbeiten. Was heißt das für den Softwerker? Wenn man weiß, daß irgendwann die großen Refactorings anstehen, daß zum Beispiel das Datenbank-Modell überholt wird oder ein anderes Framework verwendet werden soll; wenn man also planvoll auf irgendetwas hinarbeiten möchte, dann kann man die Reinigungsarbeiten schon vorneweg in die richtige Richtung lenken. Hierzu konkrete Anweisungen vorzugeben ist schwierig, am besten setzt man sich mit dem Team mal hin und überlegt, welches die schlimmsten Probleme sind, wie man sie beseitigen möchte und wie man da hinkommen kann.
Dazu ein einfaches Beispiel:
Sind beispielsweise Zugriffe auf eine bestimmtes Modul (eine Datenbank oder ein Service) überall im Code verteilt und auf unterschiedliche Weise implementiert, kann man ein Interface definieren, bei der nächsten Gelegenheit den ersten Zugriff aus dem Code lösen, damit das Interface mit einer Klasse unterfüttern und verwenden. Kommt der nächste Entwickler an anderer Stelle auf einen ähnlichen Zugriff, übernimmt er entweder die Implementierung seines Vorgängers oder er implemetiert eine neue Klasse für das Interface und stellt sie neben die erste. Irgendwann – wenn zeit ist – nimmt man sich die verschiedenen Implementierungen der Interface vor und konsolidiert die Implementierungen.