Warum Rewrites scheitern
Viele Unternehmen entscheiden sich für die Neuentwicklung ihrer in die Jahre gekommenen Legacy-Systeme. Hinter dieser Entscheidung steckt meistens die Hoffnung, dass beim nächsten Mal alles besser wird und sich alle Probleme des Altsystems mit der Neuentwicklung in Luft auflösen. Dieser Artikel erklärt, warum beim nächsten Mal vieles anders, aber wenig besser wird.
Was sind Rewrites?
Der Begriff Rewrite bezeichnet die Neuentwicklung eines existierenden Softwaresystems. Anders als bei einer tatsächlichen Neuentwicklung existiert das zu entwickelnde System bereits und soll mit einem ähnlichen Funktionsumfang erneut implementiert werden. Die Gründe für die Neuentwicklung sind vielfältig. Meistens ist der Hauptgrund, dass ein in die Jahre gekommenes Altsystem fehleranfällig, fragil und nur noch schwer erweiterbar ist. Andere Gründe für Rewrites sind der Mangel an Entwicklern für die verwendete Legacy-Technologie, das Auslaufen der Programmiersprache oder auch die Aufkündigung von Wartungsverträgen.
Und was heißt Scheitern?
Dieser Artikel stellt die These auf, dass Rewrites häufig scheitern. Dazu muss zunächst die Frage beantwortet werden, was Scheitern im Softwareumfeld genau bedeutet. Scheitern heißt keinesfalls, dass das zu entwickelnde System ganz und gar nicht ins Laufen kommt. Vielmehr ist Scheitern ein weicher Begriff, der sich am ehesten durch eine Reihe von Erfolgskriterien und deren Nicht-Einhaltung definieren lässt. Stellen wir uns also zunächst die Frage, was ein Softwareentwicklungsprojekt erfolgreich macht.
In erster Linie ist dies die Erfüllung der versprochenen Funktionalität, d. h. das System tut, was die Benutzer von ihm erwarten. Wird das System zudem in der geplanten Zeit und im Rahmen des geplanten Budgets entwickelt, dann ist das Projekt umso erfolgreicher.
Hinzu kommen nicht-funktionale Kriterien, wie Performance oder Erweiterbarkeit, deren Abwesenheit einer der Hauptgründe ist, weshalb sich Unternehmen für Rewrites entscheiden. Ein In-Time und In-Budget entwickeltes System, das tut, was die Benutzer wünschen, und das zudem auch noch performant und erweiterbar ist, ist schon ein ziemlich erfolgreiches System.
Wir könnten die Liste der Kriterien erfolgreicher Softwareprojekte noch beliebig fortsetzen. Aber bereits diese kurze Liste reicht aus, um aufzuzeigen, dass Softwareentwicklung relativ schnell scheitern kann. So ist es eher selten der Fall, dass ein neu entwickeltes System das tut, was seine zukünftigen Benutzer wünschen. Dieser Fall ist in Rewrite-Projekten tatsächlich weniger wahrscheinlich als in Erstentwicklungen. Schließlich ist das Altsystem eine ziemlich gute Vorlage für das, was das zukünftige System tun soll.
Anders sieht es hingegen bei den beiden Kriterien Zeit und Geld aus. Genau wie Neuentwicklungen müssen Rewrites geplant werden. Viele Teams beziehen in die Planung nur 10 Prozent der Features, d. h. nur die häufig genutzten Hauptfeatures in ihre Betrachtung mit ein und geben bei der Frage des Managements nach dem benötigten Zeitaufwand eine viel zu geringe Schätzung an. Diese häufig sehr optimistischen Aussagen werden zusätzlich dadurch befeuert, dass Entwickler stark von dem Wunsch nach Neuentwicklung getrieben sind und sich dieses in Aussicht gestellte Vergnügen keinesfalls durch eine zu hohe Schätzung wieder wegnehmen lassen wollen.
Geht es dann aber in die Entwicklung, dann merken Rewrite-Teams sehr schnell, dass ihre initiale Schätzung nur die Spitze des Eisbergs beinhaltet. Schicht für Schicht gräbt sich das Team tiefer ins Altsystem und entdeckt täglich neue Aspekte, die es zuvor nicht berücksichtigt hat.
Wenn schon Schätzen, dann bitte historisch
Eine der wenigen halbwegs validen Ansatzpunkte zum Schätzen von Rewrites ist das aus der agilen Softwareentwicklung bekannte Prinzip "Yesterday‘s Weather" [1]. Gemäß diesem Prinzip erfolgt die Schätzung auf Basis historischer Daten, wie z. B.:
- Wie viele Monate hat die Entwicklung des Altsystems bis heute gedauert?
- Wie groß war das durchschnittliche Entwicklerteam?
- Was waren die durchschnittlichen Personalkosten pro Person?
Angenommen, die Entwicklung des Altsystems wurde vor 10 Jahren gestartet und über diesen Zeitraum von durchschnittlich 5 Personen betreut. Ein Entwickler, Produktmanager oder Tester kostet im Schnitt um die 70.000 € im Jahr. Dann hat die Entwicklung des Altsystems bis heute grob geschätzt 10*5*70.000 € = 3,5 Millionen € gekostet.
Die aufgestellte Rechnung ist natürlich eine Milchmädchenrechnung, erfüllt aber durchaus ihren Zweck: Konfrontiert man das Management mit dieser leicht ausgerechneten Zahl, dann ist eine Neuentwicklung schnell vom Tisch. Und auf der anderen Seite gilt: Was lässt uns annehmen, dass eine Neuentwicklung günstiger sein sollte, als diese mal eben auf die Schnelle ausgerechnete Zahl?
Über den Wert von Bestandssoftware
Neben einer halbwegs realistischen Schätzung eines Neuentwicklungsprojekts empfiehlt sich ein genauer Blick auf den Wert des existierenden Systems. Die Altsoftware wurde nicht nur über einen Zeitraum von Jahren oder Jahrzehnten entwickelt, sondern über einen fast ebenso langen Zeitraum betrieben. Folglich verfügt die Software über jahrzehntelange Produktionserfahrung, ist ausgiebig getestet und hat unzählige Bugfixes erfahren. Selbst wenn die Software fragil und nur noch schwer erweiterbar ist, ist zumindest das, was heute da ist, stabil und tut das, was die Kunden und Benutzer erwarten.
Die Altsoftware ist codiertes Wissen und häufig die einzig valide Anforderungsquelle. Dokumentation ist – sofern überhaupt vorhanden – veraltet und damit wertlos. Die ursprünglichen Entwickler haben das Unternehmen längst verlassen oder sind bereits in Rente. Um an dieses Wissen zu kommen, muss intensiv Softwarearchäologie betrieben werden. Ein Vorhaben, was i. d. R. deutlich aufwändiger ist, als die von Product Ownern erdachten Anforderungen zu verstehen und umzusetzen.
Produktionsreife, codiertes Wissen und Geschäftserfolg zeichnen den Wert erfolgreich betriebener Systeme aus. Vor einer Entscheidung zum Neubau sollte deshalb immer die Quantifizierung dieses Werts stehen.
Parallelbetrieb
Des Weiteren ist zu beachten, dass für den Zeitraum der Neuentwicklung ein zweites Team bereitstehen muss. Schließlich muss das Altsystem mindestens noch solange betrieben werden, bis das neue System produktiv geht. Während dieser Zeit kommen neue oder sich ändernde Anforderungen dazu, die in beiden Systemen implementiert werden müssen. Das Alt-Team ist unmotiviert, weil es mit veralteten Technologien an Systemen auf dem Abstellgleis arbeiten muss. Das neue Team ist weniger erfahren mit der Materie und steht unter hohem Erwartungsdruck. Das neue Team ist zudem auf das fachliche Wissen angewiesen, das in dem Altsystem steckt. Teammitglieder des Alt-Teams erfahren so eine Doppelbelastung, da sie das bestehende System betreuen und zugleich Wissen für das neue System bereitstellen müssen.
Softwareentwicklung – ein komplexes Unterfangen
Wir haben eine Reihe von Argumenten gebracht, die erklären, weshalb Rewrites risikobehaftete und teure Unterfangen mit unsicherem Ausgang sind. Dabei haben wir einen ganz entscheidenden Punkt bisher noch unerwähnt gelassen: Softwareentwicklung ist ein komplexes Problem.
Expertenwissen hilft beim Lösen komplizierter Probleme.
In der Systemtheorie wird zwischen komplizierten und komplexen Problemen unterschieden. Ein kompliziertes Problem lässt sich mit ausreichendem Wissen relativ gut beherrschen. Ein gutes Beispiel für ein kompliziertes Problem ist eine mechanische Uhr. Eine solche Uhr besteht aus gut 1.500 Einzelteilen. Erfahrene Uhrmacher sind in der Lage, die Uhr komplett zu zerlegen und innerhalb eines vorhersagbaren Zeitraums wieder zusammenzusetzen. Expertenwissen hilft beim Lösen komplizierter Probleme.
Im Gegensatz zu komplizierten Problemen zeichnen sich komplexe Probleme durch ein hohes Maß an Überraschungen aus. Ein Beispiel für ein komplexes Problem kann man jeden Samstag um 15:30 in deutschen Fußballstadien erleben. Die Liste der möglichen Überraschungen eines Fußballspiels ist beliebig lang. Um nur einige Beispiele zu nennen:
- Was passiert wo auf dem Spielfeld?
- Wie ist die Strategie des gegnerischen Teams?
- Was macht der Schiedsrichter?
- Dringt der Trainer zu seinen Spielern durch?
- Verletzt sich einer meiner Spieler?
Bereits diese überschaubare Menge an Überraschungen macht es unmöglich, den Ausgang eines Fußballspiels vorherzusagen. Sicher hilft Expertenwissen (Top-Spieler plus Jürgen Klopp), um das Spiel in eine bestimmte Richtung zu lenken und einen Sieg wahrscheinlicher werden zu lassen, aber sicher ist in diesem Spiel nichts. Wer hätte nach dem 3:0 Sieg des 1. FC Barcelona im Hinspiel des Champions-League-Halbfinales gegen Liverpool darauf gewettet, dass Liverpool die Champions League 2019 doch noch gewinnt?
Kombiniert man komplexe Elemente, explodiert die Komplexität des Gesamtsystems.
Ganz ähnlich wie im Fußball läuft es in der Softwareentwicklung: Entwicklungsteams werden nahezu täglich mit unvorhersehbaren Überraschungen konfrontiert. Der Grund für dieses hohe Maß an Überraschungen findet sich in den drei Dimensionen der Softwareentwicklung: Anforderungen, Technologie und Menschen. Anforderungen sind vage, unzureichend verstanden und extrem veränderlich. Außerdem kommen sie häufig von verschiedenen Parteien, die unterschiedliche Interessen verfolgen. Technologie ist oft neu, fehleranfällig und nur schwer beherrschbar. Und dass Menschen und insbesondere ihr Zusammenwirken von enormer Komplexität geprägt sind, weiß wohl jeder. Alle drei Dimensionen sind für sich genommen bereits komplex. Kombiniert man komplexe Elemente zu einem Ganzen, explodiert die Komplexität des Gesamtsystems.
Es geht auch anders
Der Wert existierender Software, kombiniert mit der Komplexität und Unvorhersagbarkeit von Softwareentwicklung, lässt die Entscheidung für eine Neuentwicklung absurd erscheinen. Im besten Fall liefert eine Neuentwicklung ein dem Altsystem ähnliches System, mit deutlich weniger Produktionserfahrung und mit neuen Problemen, die wir heute noch nicht vorhersehen können. Deshalb ist es an der Zeit über Alternativen zum Rewrite nachzudenken.
Am Anfang des Umdenkprozesses steht eine Pain-Point-Analyse. In dieser Analyse geht es darum, ganz konkret zu analysieren und festzuhalten, welches die Hauptschmerzen des betriebenen Altystems sind. In 80 Prozent der von uns untersuchten Systeme landen wir bei den folgenden Top 3:
- Hohe Regressionsanfälligkeit aufgrund zu geringer Testabdeckung,
- mangelende Automatisierung, insbesondere des Deployments und
- schlechte Erweiterbarkeit aufgrund hoher technischer Schulden.
Dies sind schwerwiegende Probleme, die sich nicht einfach mal eben so abstellen lassen. Nehmen wir z. B. das Problem der geringen Testabdeckung kombiniert mit hoher technischer Schuld. Um den Code automatisiert testen zu können, muss er zuvor refaktorisiert werden. Auf der anderen Seite sind Refactorings nur bei ausreichender Testabdeckung sicher durchführbar. Hier beißt sich die Katze in den Schwanz. Die einzige Möglichkeit, den Code testbarer zu machen, ist die Durchführung unsicherer Refactorings mit dem Risiko unentdeckter Seiteneffekte. Micheal Feathers hat zu diesem Thema ein ganzes Buch geschrieben, in dem er erklärt, wie sich unzureichend getesteter Legacy Code sicher bzw. mit überschaubarem Risiko ändern und testbar machen lässt [2].
Von der Pain-Point-Analyse zum Sanierungsbacklog
Sind die zentralen Pain Points ermittelt, werden sie priorisiert. Die Priorisierung erfolgt Return-on-Investment-getrieben. Es geht darum, die Maßnahmen herauszufinden, die den größten Kosten-Nutzeneffekt für das Unternehmen haben. Technologische Aspekte, wie z. B. die Wahl der Programmiersprache, spielen dabei eine untergeordnete Rolle.
Ausgehend von der priorisierten Pain-Point-Liste wird ein Sanierungsbacklog erstellt. Ähnlich einem Scrum-Backlog ist dies eine priorisierte und geschätzte Liste von Arbeitspaketen, die dem Sanierungsteam als zentraler Anforderungskatalog dient. Sanierungsbacklog-Items sollten abgeschlossen und innerhalb eines begrenzten Zeitraums umsetzbar sein. Dafür werden messbare Akzeptanzkriterien benötigt, z. B.
- das Deployment soll in weniger als 10 Minuten durchführbar sein,
- das Deployment vermeidet Downtimes oder
- das Deployment lässt sich automatisiert auf die Vorgängerversion zurückrollen.
Umsetzung
Die Umsetzung des Sanierungsbacklog erfolgt iterativ inkrementell. Iterativ bedeutet, dass es keine große Entwicklungsphase, sondern stattdessen viele kleine, sich wiederholende Entwicklungsperioden gibt. Und inkrementell bedeutet, dass jede dieser Phasen ein produktionstaugliches Stück Software – ein sogenanntes Inkrement – liefert.
I. d. R. sind die Items des Sanierungsbacklogs Langläufer, die sich nicht im Rahmen einer einzigen Iteration umsetzen lassen. Dennoch ist es für solche Arbeitspakete wichtig, dass Teilergebnisse kontinuierlich integriert und ausgeliefert werden. Selbst wenn sie keinen unmittelbaren Mehrwert schaffen, liegt der Grund dafür auf der Hand: Änderungen an Legacy-Software führen zu Problemen und Seiteneffekten. Diese werden meistens erst dann sichtbar, wenn sie mit dem Rest des Systems integriert sind. Und dies ist der entscheidende Punkt: Wird wenig, d. h. nur die Arbeit von zwei Wochen integriert, dann sind die daraus resultierenden Probleme noch verhältnismäßig klein und mit überschaubarem Aufwand lösbar. Integriert man die Arbeit hingegen erst nach einigen Monaten, dann sind die Probleme groß und in den meisten Fällen nicht mehr handhabbar.
Branch by Abstraction
Für die Umsetzung und kontinuierliche Integration von Änderungen in Legacy-Projekten hat sich eine Reihe von Mustern etabliert, wie z. B. Branch by Abstraction. Das Muster abstrahiert fragile oder fehlerhafte Module mit Hilfe einer Abstraktionsschicht, die Aufrufe an das abzulösende Modul delegiert. Die Abstraktionsschicht wird im ersten Schritt kopiert und delegiert Aufrufe an eine Neuimplementierung des fehlerhaften Moduls (linke Seite in Abb. 1). Clients werden nach und nach mit der Fertigstellung neuer Funktionen auf das neue Modul umgestellt. Das Muster ermöglicht die Neuimplementierung auch von größeren Systemteilen unter Vermeidung abschließender Big-Bang-Releases. Sind alle Funktionen umgestellt, wird das fehlerhafte Modul sowie die erste Kopie der Abstraktionsschicht gelöscht (rechte Seite in Abb. 1).
Branch by Abstraction eignet sich für die iterative Renovierung eines in die Jahre gekommenen Monolithen. Das Vorgehen geht davon aus, dass man den Monolithen erhalten möchte. Viele Sanierungsprojekte entscheiden sich jedoch für den Wechsel auf ein neueres Architekturparadigma, was in 90 Prozent aller Fälle die Umstellung auf Microservices bedeutet. Auch bei Architekturwechseln dieser Art gilt es, risikobehaftete Big-Bang-Releases am Ende von mehreren Monaten oder Jahren zu vermeiden. Eines der gängigsten Muster zur iterativen Umstellung von Monolithen auf Microservices ist das Strangler Pattern [3].
Strangler Pattern
Die Anwendung des Strangler Pattern basiert auf der Entscheidung, den Monolithen abzulösen und die enthaltene Funktionalität nach und nach zu "erwürgen". Dazu werden herauszulösende Systemteile identifiziert und in eigenständige Services verschoben. Abb. 2 skizziert das Vorgehen am Beispiel von Modul 3, das zu Service 3 wird. Extrahierte Services erhalten über HTTP-APIs, Event-Broadcasting oder Datenbank-Trigger Zugriff auf Daten des Monolithen. Eine wichtige Prämisse des Strangler-Vorgehens ist, dass der Monolith nur noch geändert wird, wenn dies absolut notwendig ist.
Das LegacyLab
Die in diesem Beitrag beschriebenen Ideen und Szenarien basieren auf unserer langjährigen Erfahrung im Umgang mit Bestandssoftware. Aufbauend auf diesen Erfahrungen haben wir unser Vorgehensmodell LegacyLab zur Analyse, Bewertung und Modernisierung von Legacy-Systemen entwickelt.
Das LegacyLab startet mit der Analyse des oder der betroffenen Altsysteme. Mit Hilfe von Fragenkatalogen, Source-Code-, Architektur- und Deployment-Analysen ermitteln wir die Fitness der Systeme. Diese setzt sich aus einer Reihe von Kriterien, wie Source-Code-Qualität, Deployment-, Entwicklungsgeschwindigkeit oder Regressionsrate zusammen. Jedes dieser Kriterien wird für sich genommen bewertet. Aus den gewichteten Einzelkriterien ergibt sich die Gesamtfitness des Systems.
Neben Fitness und Pain Points ist die Kategorisierung des Systems ein weiteres wichtiges Ergebnis der Analysephase. Wir legen unseren Analysen die vier Kategorien Wartung, Sanierung, Neuentwicklung und Recycling zugrunde, die wie folgt definiert sind:
- Wartung: Das System ist wichtig fürs Unternehmen und muss laufen. Das System generiert wenig Einnahmen und wird mittelfristig nicht weiterentwickelt.
- Sanierung: Das System ist wichtig fürs Unternehmen und hat unmittelbaren Einfluss auf dessen Kerngeschäft. Das System wir kontinuierlich geändert und erweitert. Feature-Neuentwicklung ist zwingend erforderlich.
- Recycling: Das System ist nicht mehr wichtig, muss aber betrieben werden. Das Unternehmen verdient längst mit einem anderen System Geld, muss das Altsystem aber ohne essenzielle Einnahmen weiter betreiben, weil es zum Beispiel Verträge mit Kunden gibt, die das System nutzen.
- Neuentwicklung: Das System ist wichtig, steht aber vor dem Aus, weil zum Beispiel die Hardware nicht mehr existiert und sich auch nicht virtualisieren lässt. Andere Gründe können sein, dass die verwendete Programmiersprache ausläuft oder es schlichtweg keine Cobol-Programmierer mehr gibt.
Die Kategorisierung stellt zusammen mit Fitnessgrad und Pain Points die Basis für die initiale Befüllung des Sanierungsbacklogs. Die Kategorie nimmt dabei maßgeblichen Einfluss auf Tiefe und Inhalt des Backlogs. So ist die Automatisierung des Deployments für Wartungs- und Sanierungskandidaten gleichermaßen wichtig. Hingegen wird man größere Aufräumarbeiten als Vorbereitung für die Implementierung neuer Features nur selten in den Sanierungsbacklogs von Wartungskandidaten finden.
Die Erstellung des initialen Sanierungsbacklogs markiert das Ende der Analyse- und den Start der Umsetzungsphase. Ab hier beginnt die Arbeit am offenen Herzen. Big-Bang-Releases werden vermieden, indem Änderungen kontinuierlich integriert werden. Dies gilt auch dann, wenn Änderungen nicht unmittelbar sichtbar sind und anfänglich nur unter der Haube mitlaufen.
Fazit
Software-Rewrites sind nur selten notwendig und noch seltener erfolgreich im Sinne von In-Time und In-Budget. Die Begründung für diese Aussage liefert die komplexe Natur von Softwareprojekten. Das Versprechen, ein Softwaresystem von signifikanter Größe in einem vorgegeben Budget- und Zeitrahmen zu entwickeln und produktiv zunehmen, entbehrt jeder Grundlage. Dies gilt gleichermaßen für Neuentwicklung und Rewrites. Während Neuentwicklungen diesen Umstand weitestgehend erkannt und akzeptiert haben (Stichwort "agile Softwareentwicklung"), verfallen Rewrite-Projekte schnell zurück ins Wasserfalldenken. Der Gedanke, das scheinbar "kaputte" System mal eben in den kommenden sechs Monaten neu zu entwickeln, ist einfach zu verlockend und verleitet viele Entwickler zu der unbedachten Aussage: "Das schaffen wir".
- Wirdemann, R.: Scrum mit User Stories, 3. Auflage, Hanser, 2015
- M. Feathers: Working Effectively with Legacy Code, Prentice Hall, 2004
- Strangler Pattern: How to Deal With Legacy Code During the Container Revolution
Weitere Informationen:
- M. Fowler: StranglerFigApplication
- M. Fowler: BranchByAbstraction
Publikationen
- Spring & Hibernate: Eine praxisbezogene Einführung