In the Code: Der Kampf gegen den grossen Dreckklumpen
Zürich, 11.04.2016 – Erich Oswald
Fachartikel für inside-it.ch vom 11. April 2016
Ergon-CTO Erich Oswald über evolutionäre Softwareentwicklung, Modularisierung und Monolithen.
Die zunehmende Digitalisierung unseres Alltags beschäftigt uns mehr denn je. Nur weil alle davon reden, heisst das aber noch lange nicht, dass deshalb völlig klar wäre, wo die Reise hingeht. Strategien und Pläne sind gut, aber ohne ihre kontinuierliche Anpassung stehen wir bald im Regen. Die Fähigkeit, auf neue Realitäten rasch zu reagieren und Softwaresysteme an veränderte Gegebenheiten anzupassen, trennt die Gewinner von den Verlierern in einer digitalisierten Welt.
Seit über fünfzehn Jahren kennen wir mit der agilen Softwareentwicklung einen Weg, um mit kontinuierlichen Änderungen umzugehen. Nicht umsonst nannte Kent Beck eines seiner Bücher "Extreme Programming Explained: Embrace Change". Natürlich ist auch im agilen Universum nicht alles Gold, was glänzt. Dennoch stellen wir fest, dass Planung, Schätzung, Budgetierung und Fortschrittskontrolle dank agilen Methoden ziemlich gut mit regelmässig ändernden Anforderungen Schritt halten können. Gilt dies auch für die Qualität der entwickelten Software selbst? Passt ihre interne Struktur auch zum Zeitpunkt des Release noch zu den Anforderungen?
Wie ein Mudball entsteht
Leider zeigt die Erfahrung, dass sich in fast jeder Applikation bereits im Verlauf weniger Wochen Altlasten ansammeln. Dafür hat sich der Begriff der Technischen Schuld eingebürgert: Die Software erfüllt zwar ihren Zweck und ist sowohl korrekt als auch zuverlässig, aber die Implementierung entspricht nicht derjenigen, die das Team wählen würde, wenn es nochmal von vorn beginnen könnte. Ein Team durchläuft mit jedem nicht-trivialen Softwareprojekt einen Lernprozess; da ist es nicht erstaunlich, wenn es dabei weiser wird und gerne auf frühere Entscheide zurückkommen würde. Meistens sind diese Altlasten nur kleine Schönheitsfehler, die keine ernsthaften Konsequenzen haben. Solange es sich um lokal begrenzte Fälle handelt, reicht normalerweise die bewährte Pfadfinderregel, dass der nächste Programmierer, der am betroffenen Programmteil Änderungen vornimmt, den Zeltplatz sauberer zurücklässt, als er ihn vorgefunden hat, sprich wenigstens einen Teil der Mängel behebt.
Gefährlicher sind Mängel, welche die Architektur der Lösung betreffen, also die interne Struktur der Lösung und das Zusammenspiel ihrer Komponenten. Tendenziell liegt bei der agilen Entwicklung der Fokus gern auf kurzfristiger Wertsteigerung durch neue Funktionalität. Es besteht die Gefahr, dass dabei in der Hitze des Gefechts sinnvolle Verbesserungen am Lösungsdesign vernachlässigt werden. Nicht selten muss dann nach einigen Monaten ein "Big Ball of Mud" diagnostiziert werden. Das Problem am grossen Dreckklumpen ist, dass ihn niemand mehr richtig durchschaut. Wer ihn anfassen muss, macht das am liebsten an der Oberfläche und vermeidet nach Möglichkeit, mit beiden Händen tief hinein zu greifen. Bereits mittelgrosse Änderungen am System sind mit Risiken und Unwägbarkeiten verbunden und machen seine Wartung zunehmend teurer und schlechter planbar.
Wie können wir den Big Ball of Mud verhindern? Der traditionelle Ansatz des Wasserfallmodells umgeht ihn, indem nach vollständiger Aufnahme aller Anforderungen eine dafür optimale Lösungsarchitektur definiert und in späteren Phasen umgesetzt wird. Leider bleibt diese vollständige Kenntnis aller Anforderungen für die meisten von uns eine Utopie. Der Markt drängt uns, unseren Kunden in kurzen zeitlichen Abständen neue Releases zu liefern, bevor uns die Konkurrenz überholt oder unsere Startup-Finanzierung ausläuft. Eine frühzeitig definierte starre Architektur wird neuen Problemstellungen mit der Zeit immer weniger gerecht.
Das komplette Gegenteil schlägt das Extreme Programming (XP) vor: Wir beginnen mit einer minimalen Architektur und passen diese mit jeder Änderung an die neue Situation an. Die sich so schrittweise herausbildende Architektur erhält ihre Konturen erst im Verlauf der Zeit, passt dafür aber jederzeit perfekt zur Problemstellung. Einem kompetenten Team von Extreme Programmers, die über das volle Vertrauen ihres Auftraggebers verfügen, traue ich ein solches Vorgehen tatsächlich zu. Bei allen anderen Teams ist das Risiko beträchtlich, dass Termin- und Kostendruck oder Fluktuationen im Team dazu führen, dass sich die Strategie nicht aufrechterhalten lässt und wir wieder bei der grossen Dreckkugel landen.
Evolutionäre Architektur: Lieber viele kleine als ein grosser Klumpen
Die erfolgversprechendste Variante besteht deshalb darin, die Architektur zu Beginn so konzipieren, dass zwar wichtige Weichen gestellt werden, aber die für ihre Evolution nötige Flexibilität bestehen bleibt. Für eine solche evolutionäre Architektur ist weniger die erste Version wichtig als vielmehr die Spielregeln, die bestimmen, wann und wie sie sich adaptieren soll.
Das charakteristische Merkmal des Big Ball of Mud ist der Mangel an interner Struktur, der bewirkt, dass das System nur als komplexes Ganzes verstanden werden kann. Das einfachste Gegenmittel ist eine strikte Modularisierung des Systems, also das System in kleinere Komponenten aufzuteilen. Die Grenzen und Verbindungen zwischen den Komponenten sind dabei nicht beliebig, sondern bewusst gewählt. Jedes Modul zeigt gegen aussen eine minimal nötige Schnittstelle, über die es angesprochen wird, und versteckt gleichzeitig Details der Implementierung, die ohne Auswirkungen für andere geändert werden können. Das System lässt sich durch das Zusammenspiel der Module über ihre Schnittstellen beschreiben, ohne dass man jedes Modul im Detail kennen muss. Wir können das mit der Mannschaft einer Rennyacht vergleichen: Aufgaben und Kommunikationswege sind klar definiert, aber die Bewegungen der einzelnen Hände und Füsse brauchen wir nicht zu betrachten, um das System zu verstehen.
Auch in einem modularisierten System hat jedes Einzelteil das Potential, sich zu einem Dreckklümpchen zu entwickeln, aber dieses bleibt überschaubar und kann isoliert vom Rest aufgeräumt oder ersetzt werden. Ein Modul ist selber ein System für sich, das ab einer gewissen Komplexität in kleinere Komponenten aufgeteilt wird. Im Idealfall ergibt sich ein organisches Wachstum, bei dem bestehende Module in einer Art Zellteilung genau dann aufgeteilt werden, wenn sie kurz davor stehen, zu komplex zu werden. Konkret bedeutet das, dass wir explizit eine neue Schnittstelle definieren und jegliche Nutzung von Code im neuen Modul nur noch über diese Schnittstelle erlauben. In der Praxis müssen wir dazu Code verschieben und Aufrufe dieses Codes anpassen, in grösseren Systemen zum Beispiel auch den Buildprozess erweitern oder ein Datentransferformat definieren, wenn wir das neue Modul über eine Netzwerkverbindung ansprechen wollen. Designprobleme wie zyklische Abhängigkeiten zwischen Komponenten können die Aufgabe zusätzlich erschweren.
Modularisierung: Richtiges Timing, richtige Modulgrenzen
Wenn die Lösung so naheliegend ist, warum treffen wir dann in der Praxis doch immer wieder Systeme mit ungenügender Modularisierung an? Eine erste Herausforderung besteht darin, den richtigen Zeitpunkt für die Einführung eines neuen Moduls zu erkennen. Eine zu frühe Aufteilung bringt mehr Schaden in Form von erhöhter Komplexität als Nutzen. Zudem bedingt die Abspaltung eines neuen Moduls den Einsatz von Arbeitszeit, die für Kunden und Auftraggeber keine direkt sichtbaren Auswirkungen hat. Weil das System zu dem Zeitpunkt noch keine Probleme verursacht, ist die Versuchung gross, stattdessen neue Funktionalität einzubauen und die technische Schuld zu erhöhen. Wenn die Komplexität des Systems später tatsächlich Probleme macht, ist der geschätzte Aufwand für den Umbau noch grösser geworden. Möglicherweise wird er deshalb erneut zurückgestellt, und die Komplexität wächst weiter. Dieser negative Feedback-Loop führt uns früher oder später zum Big Ball of Mud.
Selbst wenn die Aufteilung zu einem guten Zeitpunkt erfolgt, heisst das nicht, dass der Schnitt an der richtigen Stelle passiert. Falsche Modulgrenzen und schlecht konzipierte Schnittstellen sind schlimmer als gar keine Modulgrenzen. Dies ist einer der Gründe, warum es zu Beginn eines Projekts so schwierig ist, eine passende Struktur zu finden. Die bekannte Methode, Komponenten entlang technischer Schichten aufzuteilen, ist zum Beispiel selten die beste Art, ein System zu modularisieren. Eine bessere Richtlinie besteht darin, die Grenzen entlang den Konzepten der Geschäftsprozesse zu ziehen, weil diese in sich konsistent und schlüssig sind und sich im Gegensatz zur Technologie nur selten grundlegend ändern. Die Methode des domänengetriebenen Entwurfs (Domain-driven Design, kurz DDD) bietet exakt dafür das nützliche Konzept der sogenannten Bounded Contexts.
Monolith aufteilen oder gleich mit Microservices starten?
Im vergangenen Sommer war ein interessanter Austausch zwischen Martin Fowler und Stefan Tilkov zu verfolgen, in dem es um die Frage ging, ob es für den Aufbau einer Microservice-Architektur besser sei, mit einem Monolithen zu beginnen und diesen später aufzuteilen oder den Monolithen von Anfang an wegzulassen. Der passendste Kommentar dazu stammt von Simon Brown: "If you can't build a monolith, what makes you think microservices are the answer?" Wer keine klaren internen Strukturen fertig bringt, wird den Big Ball of Mud auch mit Microservices nicht loswerden. Erst mit einer konsequenten Modularisierung wird ein auf Microservices basierendes System realistisch, das sich sowohl an neue Anforderungen als auch an sich ändernde Lastverteilungen flexibel anpasst.
Es müssen aber gar nicht immer Microservices sein; für viele Applikationen sind grobgranulare Komponenten wie beispielsweise Self-contained Systems oder halt tatsächlich ein Monolith sinnvoller. Von einer konsequent modularisierten Struktur, die grossen Dreckklumpen keinen Raum lässt, aber einer evolutionären Entwicklung keine Steine in den Weg legt, profitiert sowieso jedes Softwareprodukt.
Erich Oswald ist Chief Technology Officer beim Zürcher Softwarehersteller Ergon.