
Als Big Ball of Mud (BBoM) bezeichnet man ein Softwaresystem, das keine durchgängige Architektur besitzt, viele unerwünschte Querbeziehungen zwischen Modulen, Klassen und Funktionen aufweist und in Bezug auf Änderungen sehr fragil und fehleranfällig ist. Der BBoM gilt als Architektur-Anti-Pattern und wird oft bei Legacy-Systemen angetroffen.
Die Bezeichnung Big Ball of Mud wurde durch einen gleichnamigen Artikel aus dem Jahr 1997 von Brian Foote und Joseph Yoder geprägt:
A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle. These systems show unmistakable signs of unregulated growth, and repeated, expedient repair. Information is shared promiscuously among distant elements of the system, often to the point where nearly all the important information becomes global or duplicated. The overall structure of the system may never have been well defined. If it was, it may have eroded beyond recognition.
Programmers with a shred of architectural sensibility shun these quagmires. Only those who are unconcerned about architecture, and, perhaps, are comfortable with the inertia of the day-to-day chore of patching the holes in these failing dikes, are content to work on such systems.
Brian Foote & Joseph Yoder
Charakteristika und Entstehung
Ein BBoM ist meist gleichzeitig ein Legacy-System. Ein solches System hat viele Jahre lang gute Dienste erwiesen. Oft gibt es eine Wahrnehmungskluft zwischen Endanwendern und Entwicklern. Auf Anwenderseite wird eine solche Software als nützlich und erfolgreich wahrgenommen, während den Entwicklern Wartung und Umsetzung neuer Features sehr viel Mühe bereiten. Finanziell ist die Software auch erfolgreich. Sie ist eine wahre Cash Cow oder bringt zumindest einen großen Mehrwert, denn andernfalls wäre sie schon lange eingestellt oder durch etwas Anderes ersetzt worden. Natürlich gibt es auch Altsysteme, die überwiegend nachhaltig gewachsen sind, früher oder später werden dennoch oft Modernisierungsmaßnahmen versäumt oder zu weit zurückgestellt und es zeichnet sich ab, dass das System nicht auf ewig weiter betrieben und entwickelt werden kann. Eine Extremform eines solches Legacy-Systems ist der BBoM.
Damit es überhaupt zu einem BBoM mutieren kann, muss ein System eine gewisse Reife und Größe erreicht haben. Die Frage, ob es sich bei einem Altsystem um ein BBoM handelt oder nicht, muss nicht zwingend mit Ja oder Nein beantwortet werden. Es mag auch sein, dass gewisse Tendenzen vorliegen. Werden diese früh genug erkannt und ernst genommen, können sie adressiert und das System damit für weitere Jahre wartbar gehalten werden.
Ein typischer BBoM lässt vor allem viele Merkmale eines gesunden Softwaresystems vermissen. Makro- und Mikroarchitektur wirken zufällig und sind nicht konsistent. Für die Unterbringung neuer Features gibt es nicht einen logischen Ort, sondern viele verschiedene Optionen, die alle Nachteile aufweisen. Eine klare Abgrenzung von Modulen oder Klassen hinsichtlich ihrer Verantwortlichkeiten und Beziehungen zueinander ist nicht erkennbar. Die Testabdeckung ist sehr dürftig oder gar nicht vorhanden.
Die Dokumentation, wenn verfügbar, ist veraltet oder lückenhaft. Zur Weiterentwicklung des Systems wurden keine Prinzipien, Guides oder Best Practices formuliert. Vielleicht gab es sie auch mal, sie werden aber schon seit langem nicht mehr eingehalten. Code Reviews und Pair Programming werden nicht oder nur sehr rudimentär, etwa mit dem alleinigen Fokus auf Fehlervermeidung, gelebt. Den Beteiligten fehlt es an Erfahrung. Auf der einen Seite gibt es kein nachhaltiges Produktmanagement, auf der anderen Seite fehlt Architekturbewusstsein.
Im Entwicklungsteam herrscht keine gute Stimmung, es gibt viel Fluktuation und auf Onboarding wird wenig Wert gelegt. Das Deploy- bzw. Releaseverfahren ist aufwändig und mit vielen manuellen Schritten verbunden. Es existiert eine allgemeine Deployment-Angst, weil das Risiko, neue Fehler einzuführen, hoch ist.
Mit der Zeit häufen sich die technischen Schulden immer weiter an, zum einen weil es Druck „von oben“ gibt, schneller zu liefern, zum anderen weil aufwändige, aber notwendige Aktualisierungen als zu teuer angesehen und immer wieder aufgeschoben werden.
Populäre Anti-Patterns in einem Big Ball of Mud
In einem BBoM kann man eine Menge an Anti-Patterns antreffen. Einige davon stelle ich in den folgenden Abschnitten vor.
Beim Gottobjekt bzw. der Gottklasse (God Object / God Class) handelt es sich um eine Evolution globaler Variablen und Helferklassen. Ein Gottobjekt hat keine klar definierte Verantwortung und zu viel Wissen. Das Gottobjekt ist eine bequeme Abkürzung, die aber gegen die Prinzipien der losen Kopplung und hohen Kohäsion verstößt. Gottobjekte werden aufgrund ihres potenziell unbegrenzten Wachstums schnell unübersichtlich, erschweren Refactorings und erhöhen das Risiko von Bugs, da sie Beziehungen zu vielen Klassen und oft auch ihren Interna haben.
Spaghetti-Code ist ein Begriff für schwer lesbaren und damit auch schwer wart- und erweiterbaren Code. Ursprünglich geht der Begriff auf eine Überverwendung von Labels bzw. Goto-Statements zurück, die Lesbarkeit kann aber auch durch eigenwillige Abgrenzungen von Funktionen stark beeinträchtigt werden. Ein wichtiges Element sind hier die Namen. Spiegeln diese nicht wider, was tatsächlich passiert, ist der Umfang der Funktion auf den ersten Blick nicht ersichtlich, ruft diese Funktion weitere undurchsichtige Funktionen auf, die wieder weitere ebenfalls nicht klar abgegrenzte Funktionen aufrufen und handelt es sich dabei noch um einen Algorithmus, der auch auf den zweiten Blick nicht verständlich erscheint, so hat man auch hier eine Form von Spaghetti-Code, insbesondere, wenn die inneren Funktionen auch wiederum aus anderen Teilen der Anwendung aufgerufen werden. Eine besondere Form von Spaghetti-Code ist der Lasagne-Code, bei dem ein Aufruf in mehrere Schichten unterteilt wird und die Verantwortlichkeit der verschiedenen Schichten nicht klar festgelegt ist bzw. dagegen verstoßen wird, im schlimmsten Fall sogar bidirektionale Abhängigkeiten zwischen diesen Schichten vorliegen. Ein einfaches Beispiel dafür ist eine Anwendung, die in Präsentations- Geschäftslogik- und Persistenzschicht unterteilt ist, aber auch Geschäftslogik in der Präsentationsschicht enthält, sich SQL-Statements in der Geschäftslogikschicht wiederfinden und selbige aus der Persistenzschicht heraus aktiv aufgerufen wird.
Vom Lava Flow (Lavafluss) spricht man, wenn ein Funktionsaufruf sich seinen Weg durch den Code bahnt und dabei bestimmten Code ausführt und anderen gewissermaßen umschifft. Ein Lava Flow kann aus einem falschen Verständnis von DRY heraus entstehen, wenn zwei Algorithmen zusammengeführt werden, die nicht wirklich zusammengehören. So wird je nach Eingabewert ein Teil der Logik ausgeführt und ein anderer Teil nicht. Ein viel größeres Potenzial haben aber nachträgliche Erweiterungen bereits viel zu langer Funktionen, die dann einfach um einen weiteren Anwendungsfall ergänzt werden. Auch aus mangelndem Verständnis des Codes heraus kann ein Lava Flow entstehen: Existiert in der aktuellen Teamzusammensetzung kein Verständnis mehr zu einer bestimmten Fachlogik, kann es dazu kommen, dass bei Umsetzung einer fachlichen Anforderung eine bestehende Geschäftslogik verwendet werden soll, gewisse Aspekte dieser aber nicht ausgeführt werden dürfen und in der Umsetzung die Funktion einfach um ein Flag erweitert wird, so dass im neuen Anwendungsfall ein bestimmter Teil der Geschäftslogik umgangen wird. Ein besonderer Fall von Lava Flow ist toter Code (dead code). Im besten Fall ist dieser offensichtlich und kann entfernt werden, im schlimmsten Fall führt das Entfernen zu einem Bug, weil Code via Reflection aufgerufen wird und damit nicht bekannt ist, ob und in welchen Fällen dieser Code noch durchlaufen wird.
Copy & Paste ist ein weiteres Anti-Pattern, welches oft in BBoMs angetroffen werden kann. Dabei wird bestehender Code kopiert, ggf. leicht modifiziert und an anderer Stelle wiederverwendet. Copy & Paste wird oft aus Bequemlichkeit eingesetzt, kann aber auch aus der Angst heraus entstehen, beim Refactoring einen Fehler zu verursachen. Wenn von Projektleitungsseite ein Null-Fehler-Kultur vorgelebt wird oder kein Verständnis existiert, wie Softwareentwicklung funktioniert, fällt leicht die Entscheidung für Copy & Paste, da jeder Fehler negativ auf den Entwickler zurückfällt, es für eine saubere Umsetzung aber keine Anerkennung gibt. Copy & Paste führt schnell zu Fehlern, wenn eine Kopie beim Umsetzen einer Änderung übersehen wird. Gleichzeitig wirkt sich Copy & Paste negativ auf die Entwicklungs-/Wartungskosten und time to market aus, da die einmalige Umsetzung einer Änderung wesentlich schneller vonstatten geht, als die mehrfache.
Beim Anti-Pattern Magic Numbers spricht man von der Verwendung von Zahlen im Quellcode, ohne dass erkennbar ist, warum diese konkrete Zahl benutzt wird und was die Auswirkung wäre, würde man diese Zahl durch eine andere ersetzen. Als Beispiel könnte man eine Anweisung nennen, die irgendwelche Array-Operationen durchführt und dabei hart kodiert die Zahlen 97 und 122 verwendet. Dass es bei der Operation um Großbuchstaben geht, sieht man erst auf den zweiten Blick oder wenn man sich mühsam im Debugger durch den Code arbeitet. Sprechender wäre es, diese „magischen“ Zahlen in zwei Konstanten ASCII_LOWERCASE_A=97
und ASCII_LOWERCASE_Z=122
auszulagern.
Was ein Big Ball of Mud zur Folge hat
Auf dem Weg hin zu einem BBoM spart man mittelfristig Geld, denn Qualität kostet Geld und eine wohlüberlegte Architektur zu definieren und zu entwerfen kostet erstmal mehr Zeit als eine zufällige entstehen zu lassen. Das Schreiben von automatisierten Tests, Linting und statische Codeanalyse kosten ebenso Zeit und Geld, Code Reviews und Pair Programming noch viel mehr.
Möchte man eine Wegwerfsoftware mit überschaubarem, klar abgegrenzten Umfang schreiben, deren Lebensdauer zeitlich klar begrenzt ist, kann man durchaus an der Qualität sparen. Die Gefahr liegt aber darin, dass ein Prototyp oder Proof of Concept diese Phase eben doch überdauert und viele Jahre lang weiter ausgebaut und verwendet wird.
Qualität ist eine Investition und zu Beginn teuer, zahlt sich im Laufe der Zeit aber immer mehr aus. Kleine Funktionen in einem gut gewachsenen, qualitativ hochwertigen System zu ergänzen, kostet in der Regel nicht viel. Auch Tests und weitere Qualitätssicherungsmaßnahmen sind oft vor allem eine Anfangsinvestition. Die richtige Architektur und die richtigen Qualitätssicherungswerkzeuge müssen erstmal definiert und ausgewählt werden. Hat das System bereits eine gute Testabdeckung, so kann es sein, dass für eine fachliche Änderung nur ein bestehender Test in zwei Zeilen angepasst werden muss oder als Vorlage einfach kopiert und leicht modifiziert werden kann. Das Schreiben der Tests ist dann mitunter eine Sache von wenigen Minuten. Wenn der Release-Prozess komplett automatisiert ist, bindet das Deployment einer neuen Funktion im besten Fall überhaupt keine menschlichen Ressourcen und kostet nur wenige Minuten Rechenzeit für den CI/CD-Runner.
Spart man an der Qualität, ist spätestens nach wenigen Jahren der Punkt erreicht, wo die Gesamtkosten der Produktentwicklung höher sind als wenn man einen qualitativ soliden Ansatz gewählt hätte. Irgendwann ist ein Kipppunkt erreicht, wo jede Erweiterung das System nur noch schlechter macht. Diesen Prozess umzukehren ist dann mit einem enormen Aufwand verbunden. Anpassungen kosten dann aber nicht mehr einfach nur viel Geld, sie machen sich durch häufige Bugs auch gegenüber dem Kunden bemerkbar und verursachen zusätzliche Kosten im Betrieb und bei den Fachbereichen, weil auf ein neues Release oft ein oder mehrere Hotfixes kurz hinterher kommen und im schlimmsten Fall Daten händisch korrigiert oder wiederhergestellt werden müssen, weil sie ein fehlerhaftes Release kaputt gemacht hat. Mitunter wird die Software auch zu einem Sicherheitsrisiko, weil damals der Umstieg von Python 2 auf Python 3, oder von AngularJS auf Angular 2+ versäumt wurde, oder es handelt sich um eine Desktop-Anwendung, die nur noch mit viel Tricksereien unter Windows 10 lauffähig gemacht werden kann und auf Windows 11 entgültig nicht mehr einsetzbar ist.
Ein BBoM kann im schlimmsten Fall zu einem Existenzrisiko werden, gerade wenn es sich bei dem System um eine Cash Cow handelt. Wenn man Glück hat, lässt sich die Software Schritt für Schritt nach dem Strangler Pattern ablösen, funktioniert dies aber nicht, weil man sich etwa von einem mittlerweile hoffnungslos veralteten Framework abhängig gemacht hat, dass sich vom Frontend bis zur Datenbank durch das gesamte System zieht, so bleibt nur der Neuanfang auf der grünen Wiese. Muss man sich für diesen Schritt entscheiden, so muss einem klar sein, dass das neue System über lange Zeit hinweg von den Endanwendern als schlechter als das alte System wahrgenommen wird. Unter Umständen wird es sehr schwierig, Bestandskunden zu überzeugen, auf das neue System zu wechseln. Da es in einem BBoM keine Trennung von Technik und Fachlichkeit gibt, man auf der grünen Wiese aber die bisherige Fachlichkeit weiter abbilden möchte, kann man hoffentlich auf eine gute fachliche Dokumentation zurückgreifen, oder man muss sich im schlimmsten Fall alles neu erarbeiten. Eines steht fest: Ein Neuanfang auf der grünen Wiese ist mit enormen Kosten und einem erheblichen Risiko verbunden.
Wie man der Entstehung eines Big Ball of Mud entgegenwirkt
Ich kann diese Fragestellung auf vielen Ebenen erörtern. IT-Kompetenz in der Führung, eine gute Kultur, die richtige Projektvorgehensweise und ein angemessenes Produktmanagement zahlen alle darauf ein, ob aus einem System irgendwann ein BBoM wird oder nicht. Ich lege in meinem Blog aber den Fokus auf Entwicklung und Architektur und werde deshalb eine Ebene tiefer gehen.
Für mich ist das Miteinander im Team genauso wichtig wie vereinbarte Designprinzipien und Verständnis für die Geschäftsdomäne bzw. der Blick auf das Gesamtsystem. Ich beginne mit ersterem.
Gute Teams produzieren meiner Meinung nach fast immer bessere Ergebnisse als Einzelkämpfer. Es ist daher wichtig, dass das Team sich gut genug kennt um miteinander vertrauensvoll und wertschätzend arbeiten zu können. Je mehr die Teammitglieder aufeinander eingestimmt sind, desto einfacher wird es, auf einen gemeinsamen Nenner zu kommen. Bedeutsame Entscheidungen sollten immer im Team beschlossen werden, mindestens nach dem Konsent-Prinzip, so dass jeder im Team sein Veto einlegen kann. Ein gemeinsames Verständnis vermeidet Frust und fördert konsistenten, gut lesbaren und erweiterbaren Code.
Ohne Code Reviews und/oder Pair Programming kann man meiner Meinung nach nur zu einem gewissen Grad aufeinander abgestimmt sein. Regelmäßiges Pair Programming führt zu mehr Verständnis für Ansätze und Denkweisen des Pairing-Partners und erweitert den eigenen Horizont. Dabei sollte jeder im Team mit jedem pairen können. Je mehr man im Pairing unterwegs ist, desto schneller kommt man auf einen gemeinsamen Nenner, wenn Entscheidungen getroffen werden müssen. Auch wenn keine agile Vorgehensweise ausgewählt wurde, halte ich regelmäßige Rückblicke in Form von Retrospektiven und Reviews des aktuellen Software-Inkrements für sinnvoll. Je mehr man sich ausspricht, je zeitnaher man Feedback erhält, umso besser kann man vermeiden, dass sich mit der Zeit Frust anstaut oder sich schlechte Praktiken verfestigen. Die intrinsische Motivation hoch zu halten, Fluktuation auf ein Minimum zu begrenzen und Wissensinseln zu vermeiden, sind für mich wesentlich, um eine Software mittelfristig gesund wachsen zu lassen.
Bei der Entwicklung im Kleinen sollte man sich Orientierungspunkte erarbeiten, auf die sich alle einigen können, um konsistenten, nachhaltigen Code zu produzieren. Hier kann man sich darauf verständigen, gängige Best Practices wie Separation of Concerns, Information Hiding, lose Kopplung und hohe Kohäsion hochzuhalten, SOLID einzuhalten, direkt nach Clean Code zu entwickeln und Praktiken wie die Boy Scout Rule, „hinterlasse den Code ordentlicher als du ihn vorgefunden hast“, zu etablieren.
Je nach Software und Domäne kann es sinnvoll sein, nach Domain-driven Design vorzugehen, insbesondere dem Strategic Design, oder eine hexagonale Architektur zu wählen. Das gesamte Team sollte ein gutes Verständnis der Domäne haben, um nachhaltige Entscheidungen treffen zu können. Methodiken wie das Event Storming können dabei helfen, Verständnis für die Domäne und neue geschäftliche Abläufe zu vermitteln.
Grundsätzlich würde ich anvisieren, so viel wie möglich zu automatisieren, um Fehler zu vermeiden und Aufwände zu reduzieren. Eine Technologie wie Testcontainers kann unterstützen, automatisierte Integrationstests durchzuführen, mit einem Werkzeug wie ArchUnit lässt sich die Einhaltung der definierten Architekturvorgaben messen. Werkzeuge zum Linting und zur Messung der Code Coverage können eine hohe Qualität weiter unterstützen.
Früher oder später wird es Fluktuation im Team geben, daher sollte auch die Dokumentation nicht vernachlässigt werden. Mir ist es immer am liebsten, wenn der Code selbsterklärend ist und nicht weiter kommentiert werden muss, um eine minimale Dokumentation kommt man aber nicht drum rum. Um sich nicht von großen Systemen wie Atlassian Confluence abhängig zu machen, kann man für die Entwicklerdokumentation auf ein Format wie Markdown oder AsciiDoc setzen. Um neuen Teammitgliedern besser verständlich zu machen, warum bestimmte Entscheidungen getroffen wurden und ob diese immer noch valide sind, können Architekturentscheidungen dokumentiert werden.
Befindet sich die Software in der Cloud und hat einen nicht unerheblichen Anteil Infrastruktur, ist eine enge Zusammenarbeit zwischen Entwicklung und Betrieb sinnvoll. Für Störungen sollten Post-Mortem-Analysen geschrieben werden, um aus Fehlern lernen zu können und sie zukünftig nicht zu wiederholen. Damit nachhaltige Entscheidungen getroffen werden können, sollten Qualitätsmerkmale definiert und Qualitätsszenarien formuliert werden. So lässt sich fundiert begründen, welche Optimierungen, etwa hinsichtlich CPU, Speicher oder Netzwerk, sinnvoll sind und welche Szenarien (z.B. Ausfall der Datenbank) als wie realistisch eingeschätzt werden und inwiefern Vorkehrungen getroffen werden sollten.
Die genannten Vorschläge sind in keiner Weise abschließend, sondern sollen viel mehr ein Gefühl vermitteln, worauf es ankommt. Je nach Umfeld stehen andere Aspekte im Mittelpunkt. Aus meiner Sicht sind drei Dinge entscheidend, um das Risiko eines BBoM zu minimieren: die richtigen Kompetenzen, die richtigen Werte und die Unterstützung der Führung, das Team zu befähigen, eigene und nachhaltige Entscheidungen zu treffen.
- Webseite von Brian Foote