Legacy-Systeme: Von nützlicher Software zum nutzlosen Artefakt
Dieser Artikel ist eine Übersetzung. Das Original des Softwareentwicklers und Bloggers Rajiv Prabhakar ist hier(öffnet im neuen Fenster) zu finden.
Es gibt ein Muster, das ich sehr oft in Software-Teams sehe:
Es existiert die Notwendigkeit, das bestehende Systemverhalten zu ändern, um eine neue Funktionalität zu erreichen.
Der Softwareentwickler erkennt, dass das vorhandene Design für die erforderliche Änderung nicht gut geeignet ist. Er schlägt Designänderungen und eine Restrukturierung vor, als Teil der Implementierung der gewünschten Funktionalität.
Die Kollegen überprüfen die Arbeit und machen sich Sorgen, dass die vorgeschlagenen Änderungen kritisch sein könnten. Da niemand im Team das System wirklich gut versteht, befürchten sie, dass jede Restrukturierung Fehler verursacht. Sie hatten das schon mal und sind entschlossen, es nicht noch einmal zu riskieren.
Sie versuchen also, signifikante Änderungen zu vermeiden. "Fügen Sie einfach hier, in der Methode Foo in der Klasse Bar, eine weitere if-Prüfung, einen Service-Aufruf und/oder eine Abhängigkeit zur Klasse Baz hinzu." Die Änderungen bestehen nur aus ein paar Zeilen Code – weit weniger riskant.
Die Klasse Bar und die Methode Foo waren aber nie dazu gedacht, Service-Aufrufe durchzuführen oder Ergebnisse aufgrund einer Abhängigkeit von Baz zu filtern. Es verstößt gegen den ursprünglichen Entwurf der Klasse. Aus einer einzigen Aufgabe der Klasse werden nun mehrere, denn aus "Bar gibt immer ABC zurück" wird "Bar gibt immer ABC zurück, es sei denn, DEF gibt in diesem Fall XYZ zurück". Die Trennung von Zuständigkeiten weicht einer Überschneidung der Zuständigkeiten.
Dennoch: Es sind nur ein paar zusätzliche Codezeilen, wie schlimm kann das schon sein? Außerdem ist das Risiko gering, schließlich fassen wir nur sehr wenig Code an. Es ist schwer, auf Basis abstrakter Designprinzipien dagegen zu argumentieren. Schließlich sind wir hier Pragmatiker, keine Architektur-Astronauten!
Und so beschließt das Team, den Weg des geringsten Widerstands zu gehen. Es aktualisiert pflichtbewusst die Unit-Tests. Außerdem testet es die relevanten Änderungen manuell, denn es weiß aus Erfahrung, dass Unit-Tests nur die halbe Miete sind. Der Code wird erfolgreich und ohne Probleme ausgerollt, alle atmen auf und beglückwünschen einander zu einer gelungenen Arbeit.
Irgendwann ist das System nur noch zum Bestaunen gut
Dieser Prozess wiederholt sich jedes Mal, wenn eine Änderung gemacht werden muss. Mit jeder Iteration wird das Design ein bisschen verwaschener, das System ein wenig schwieriger zu verstehen – und es verhält sich jedes Mal einen Tick unvorhersehbarer. All das macht das Team noch risikoscheuer und entschlossener, unnötige Änderungen zu vermeiden. Was den Verfall weiter beschleunigt.
Und jedes Mal, wenn jemand das Team verlässt und jemand Neues kommt, erhält diese Verfallsbeschleunigung noch einen Schub.
Ziemlich bald hat man ein vollwertiges Legacy-System, eine Legacy-Software, über die sich jeder gerne beschwert und in der niemand herumfuhrwerken möchte. Die Codebasis ist kein lebendiges System mehr, sondern ein Museumsartefakt. Sie wird mit Staunen und Bewunderung betrachtet, aber auf keinen Fall wird sie angefasst.
Wie Legacy-Software überhaupt entsteht
In Working Effectively with Legacy Code(öffnet im neuen Fenster) (Effektiv mit Legacy-Code arbeiten) beschreibt Michael Feathers Legacy-Software als jedes Softwaresystem, das nicht getestet wird. Diese Beobachtung ist sehr aufschlussreich, ich würde aber noch einen Schritt weiter gehen: Für mich ist Legacy-Software jede Software, an der Entwickler aus Angst vor Fehlern keine Änderungen mehr machen.
Die beiden häufigsten Gründe für die Abneigung gegen Veränderungen sind:
- Komplexität
- Lücken in der Testabdeckung
Komplexer Code macht Tests noch zwingender. Und Löcher in der Testabdeckung machen deutlich, dass die automatisierte Testsuite nicht verlässlich ist.
Wenn man sie nach dem Testen fragt, betonen die meisten Software-Teams, dass sie sehr diszipliniert sind, wenn es darum geht, Tests zu schreiben, die alle Funktionen abdecken. Um wirklich herauszufinden, wie gut ihre Testsuite ist, reicht aber eine Antwort auf diese einfache Frage: Wie viel Zeit verbringt das Team mit manuellen Tests?
Meiner Erfahrung nach gibt es viele Teams, die behaupten, sehr diszipliniert zu sein, die aber auch sehr viel Zeit mit manuellen Tests verbringen. Der Grund dafür ist eindeutig, dass ihre automatisierten Tests erhebliche Lücken haben(öffnet im neuen Fenster) – häufig, weil ihre Tests nur auf Unit-Tests ausgerichtet sind und nicht auf emergente(öffnet im neuen Fenster) Funktionalität, die mit End-to-End-Tests abgedeckt werden kann(öffnet im neuen Fenster).
Die mangelnde Testabdeckung verursacht einen schädlichen Loop:
Mangelndes Vertrauen in die Durchführung von Änderungen → Alle Änderungen werden ausgiebig manuell getestet → Größere Änderungen werden vermieden, weil manuelles Testen zeit- und arbeitsintensiv ist → Alle Änderungen folgen dem Weg des geringsten Widerstands → Der Code wird immer komplexer, sein Design immer schlechter und er enthält immer mehr Fehlerquellen → Mangelndes Vertrauen in die Durchführung von Änderungen.
Dieser verhängnisvolle Loop wird mit der Zeit immer schlimmer, bis man schließlich mit einem Legacy-System dasteht, das geschäftskritisch ist, mit dem man aber nur noch schlecht arbeiten kann. Es braucht immer mehr Zeit, um überhaupt noch Änderungen vorzunehmen. Schließlich kommt der Tag, an dem das System unter seinem eigenen Gewicht zusammenbricht.
Wie verhindert man, dass nutzlose Legacy-Systeme entstehen?
Die einzige Möglichkeit, diesen Teufelskreis zu durchbrechen ist, ihn an der Quelle zu bekämpfen – dem Mangel an Vertrauen in die Durchführung von Änderungen. Ich habe gesehen, wie ein Team das geschafft hat, indem es schlicht manuelle Tester eingestellt hat, die alle Änderungen vor dem Einsatz gründlich (manuell) testen sollten. Das verlangsamt zwar die Iterationszeiten und bringt höhere Personalkosten mit sich. Aber zumindest durchbricht es den Teufelskreis.
Der häufigere und ideale Weg, ist jedoch, in automatisierte Tests, schrittweises Ausrollen und die automatische Überwachung und Warnung bei Fehlern im Produktionsbetrieb zu investieren.
Es kostet viel Zeit und Mühe, automatisierte Tests als First-Class-Objekt im Entwicklungsprozess zu etablieren – vor allem, um die Werkzeuge zu entwickeln, die für qualitativ hochwertige Integrations- und End-to-End-Tests erforderlich sind. Aber die Vorteile sind enorm, denn die Entwickler müssen dann eben nicht mehr Unmengen an Zeit für manuelle Tests aufwenden.
Schrittweises Ausrollen und bessere Warnungen im Produktionsbetrieb können Bugs vielleicht nicht verhindern, aber sie können enorm dazu beitragen, sie zu entschärfen. Bei Unternehmen wie Amazon und Google werden neue Software-Builds oft erst mal nur auf einer Maschine bereitgestellt. Einer Maschine, die sich in der Produktionsreihe befindet und den Produktionsverkehr bedient, genau wie der Rest der Maschinen. Nach einer gewissen Zeit wird der Build dann inkrementell auf immer mehr Rechnern bereitgestellt, bis er schließlich auf allen zum Einsatz kommt.
Das lässt sich besonders gut mit automatischen Warnungen kombinieren. Mit einer Fail-Fast-Methode und einer Code-Konfiguration, die bei solchen Fehlern automatisch Alarm schlägt(öffnet im neuen Fenster), werden Bugs schnell entdeckt – und nicht erst Wochen oder Monate später, wenn die Kundenbeschwerden hereinkommen. In Verbindung mit einem schrittweisen Ausrollen können neue Builds auf nur wenigen Rechnern freigeben werden. Es wird überprüft, ob ein Alarm ausgelöst wird, und wenn das so ist, wird das Ausrollen zurückgenommen. So wird nur ein winziger Bruchteil der Nutzer betroffen sein – was nicht ideal, aber um Längen besser ist als die Alternative.
Sich überwinden
Wenn sich ein Team bereits in dem besagten Teufelskreis befindet, kann es sehr schwer sein, daraus auszubrechen. Die oben genannten Lösungen liefern an und für sich keinen Geschäftswert – man schiebt sie also lieber auf die lange Bank. Es ist schwer zu rechtfertigen, viel Zeit für das Testen, Überwachen und Bereitstellen von Verbesserungen für ein funktionierendes Legacy-System aufzuwenden, wenn das Marketingteam gleichzeitig darauf drängt, das nächste Killer-Feature herauszubringen, das die Nutzer begeistern und die Konkurrenz ausstechen wird.
Aus der Schleife auszubrechen, kann mit gemeinsamen Anstrengungen und mit Investitionen gelingen. Allerdings ist das den meisten Wartungsteams nur selten gegeben. Sie drehen sich also immer weiter und weiter in der Schleife ... bis alles so schlecht ist, dass beschlossen wird, das ganze Ding zu verwerfen und von Grund auf neu zu schreiben. An diesem Punkt wird ein Legacy-System in den Ruhestand versetzt und das nächste Legacy-System wird geboren.
- Anzeige Hier geht es zu Hacking & Security: Das umfassende Handbuch bei Amazon Wenn Sie auf diesen Link klicken und darüber einkaufen, erhält Golem eine kleine Provision. Dies ändert nichts am Preis der Artikel.