Monolithen mit DDD aufschneiden

Fast jedes Softwaresystem wird mit guten Vorsätzen, aber unter schwierigen Bedingungen entwickelt: Knappe Zeitvorgaben zwingen uns, schnelle Lösungen – Hacks – zu programmieren. Unterschiedliche Qualifikationen im Entwicklungsteam führen zu Code in ebenso unterschiedlicher Güte. Alter Code, den keiner mehr kennt, muss angepasst werden und vieles mehr. All das führt zu schlechtem, verknäultem Code, dem sogenannten Monolithen, der die Entwicklungskosten in Zukunft in die Höhe treibt und den Entwicklungsteams schlaflose Nächte bereitet. Mit Domain-Driven-Design (DDD) haben wir ein Werkzeug an der Hand, um solche Monolithen Schritt für Schritt zu zerlegen und wieder in den Bereich der beherrschbaren Wartungskosten zu bringen.

Entstehen von Monolithen

 

Entwicklung und Effekt von technischen Schulden. (Abb. 1)

 

Gehen wir einmal davon aus, dass wir zu Beginn unseres Softwareentwicklungsprojekts eine qualitativ hochwertige, modulare Architektur entworfen haben. Dann kann man hoffen, dass sich unser Softwaresystem am Anfang gut warten lässt. In diesem Anfangsstadium befindet sich unser Softwaresystem also in dem Korridor geringer technische Schulden mit gleichbleibendem Aufwand für die Wartung (Abb. 1 – grüne Klammer). Je länger unser System lebt, desto mehr erweitern wir es und fügen neue Funktionalität hinzu. Bleibt uns keine Zeit bei diesen Erweiterungen auf den Erhalt der modularen Architektur zu achten, so erodiert diese Architektur mit der Zeit immer mehr. Wir nehmen immer mehr technische Schulden auf (Abb. 1 – gelbe Pfeile die rot werden) und nähern uns immer mehr einem verknäulten Monolithen mit hohem, unplanbarem Aufwand in der Wartung. (Abb. 1) macht diesen langsamen Verfall dadurch deutlich, dass die roten Pfeile immer kürzer werden. Pro Zeit oder Budget schaffen wir immer weniger Funktionalität. Folgefehler sind immer schwerer nachvollziehbar, bis zu dem Punkt, an dem jede Änderung zu einer schmerzhaften Anstrengung wird. Besser wäre es, wenn unser Management und wir akzeptieren würden, dass Softwareentwicklung ein ständiger Lernprozess ist, bei dem der erste Wurf einer Lösung selten der Endgültige ist. Eine Erweiterung im Korridor mit gleichbleibendem Aufwand für Wartung führt also immer auch erst einmal zu mehr Schulden (Abb. 1 – gelber Pfeil „Wartung und Erweiterung“). Die Überarbeitung der Architektur (Abb. 1 – grüne Pfeile)  muss in regelmäßigen Abständen durchgeführt werden, wenn vermieden werden soll, dass ein Monolith entsteht. Folgt man diesem Prinzip, so läuft die Entwicklung in einer stetigen Folge von Erweiterung und Refactoring ab. Kann ein Team diesen Zyklus von Erweiterung und Refactoring dauerhaft verfolgen, so wird das System im Korridor geringer technischer Schulden bleiben (Abb. 1 – gelbe und grüne Pfeile im Korridor).

 

Modularity-Maturity-Index (MMI). (Abb. 2)

Um für Monolithen eine Bewertungsmöglichkeit zu schaffen, wie gut der Sourcecode auf eine fachliche Zerlegung vorbereitet ist, haben wir in den vergangenen Jahren den Modularity-Maturity-Index (MMI) entwickelt. In (Abb. 2) ist eine Auswahl von 21 Softwaresystemen dargestellt, die in einem Zeitraum von fünf Jahren analysiert wurden (X-Achse). Für jedes System ist die Größe in Lines-of-Code dargestellt (Größe des Punktes) und die Modularität auf einer Skala von 0 bis 10 (Y-Achse). Die Farbgebung rot, gelb und grün entspricht dabei den jeweiligen Bereichen der technischen Schulden aus (Abb. 1).

Liegt ein System in der Bewertung zwischen 8 und 10, so ist es im Inneren bereits modular aufgebaut und wird sich mit wenig Aufwand fachlich zerlegen lassen. Systeme mit einer Bewertung zwischen 4 und 8 haben gute Ansätze, aber hier sind einige Refactorings notwendig, um die Modularität zu verbessern. Systeme unterhalb der Marke 4 würde man nach Domain-Driven-Design als Big-Ball-of-Mud bezeichnen. Hier ist kaum fachliche Struktur zu erkennen und alles ist mit allem verbunden. Solche Systeme sind nur mit sehr viel Aufwand in fachlich Module zerlegbar.

Ist man erst einmal im Korridor hoher, unplanbarer Wartung (Abb. 1) oder beim MMI im Range 0-4 angelangt (Abb. 2), gibt es zwei Möglichkeiten um aus dem Dilemma wieder heraus zu kommen: Das System wird durch ein neues ersetzt (Abb. 1 – Zyklus) oder der Monolith wird refactored (Abb. 1 – rote und gelbe Pfeile abwärts). Beide lassen sich mit DDD unterstützen. In diesem Artikel geht es um die zweite Möglichkeit: Wie können wir den Monolithen mit DDD zerlegen, so dass er für unsere Entwickler wieder beherrschbar wird?

 

Typische Architektur eines Monolithen

Die meisten Monolithen, mit denen die Autorin heutzutage zu tun hat, haben eine Schichtenarchitektur, wie sie Anfang der 2000er Jahre en vogue war. Ende der 90er Jahre war das häufigste Problem, mit dem sich Softwarearchitekten herumgeschlagen haben, dass der Sourcecode von der Business-Logik, dem User-Interface und der Datenbankzugriffschicht fröhlich gemischt wurden. Insofern war die Maßgabe, dass Softwaresysteme technisch geschichtet werden sollten, ein wichtiger sinnvoller Schritt.

 

Schematische Darstellung einer Schichtenarchitektur. (Abb. 3)

In (Abb. 3)  ist eine schematische Schichtenarchitektur dargestellt, wie sie in dieser Zeit typischerweise verwendet wurde. Eine solche Schichtenarchitektur führt erst einmal grundsätzlich dazu, dass der fachliche Sourcecode (Application und Domain) vom technischen Sourcecode (User-Interface und Datenbankzugriff) getrennt wird. Es entsteht also eine technische Ordnung im System.

Für Systeme, die von einem schlagkräftigen Entwicklungsteam allein bearbeitet werden können, reicht eine Schichtenarchitektur aus. Unter einem schlagkräftigen Entwicklungsteam versteht man ein Team, das eine Größe hat, die bei Urlaub und Krankheit nicht zu Stillstand führen, also ab drei Personen, und bei dem die Kommunikation beherrschbar bleibt, also nicht mehr als sieben bis acht Personen, die sich gegenseitig auf dem Stand der Dinge halten müssen. Ein solches Team sollte ein Stück Software, ein Modul, zum (Weiter)-Entwickeln bekommen, dass unabhängig vom Rest des Systems ist. Denn nur wenn das Modul unabhängig ist, kann das Team eigenständig Entscheidungen treffen und ohne auf andere Teams und deren Zulieferungen zu warten, sein Modul weiterentwickeln.

 

Fachliche Module im Monolithen. (Abb. 4)

Die meisten Systeme, die seit Anfang der 2000er Jahre entwickelt werden, haben diese Größe längst überschritten und müssen weiter zerlegt werden. DDD empfiehlt hier, eine Aufteilung anhand von fachlichen Kriterien zu wählen: Den Monolithen also quer zu den technischen Schichten aufzuschneiden (Abb. 4). Die so entstehenden fachlichen Module werden in DDD Bounded-Context genannt.

Diese Zerlegung weist viele Vorteile auf. Denn neben der Unabhängigkeit der Teams und der modulareren und damit leichter verständlichen Struktur kann das System, wenn man es entlang der fachlichen Module in einzelne Microservices zerlegt, auch skalieren.

 

Fachliche Zerlegung

Um eine gute fachliche Zerlegung zu finden, hat es sich in Projekten als sinnvoll erwiesen, den Monolithen und die in ihm möglicherweise vorhandene Struktur erst einmal beiseite zu legen und sich noch einmal grundlegend mit der Fachlichkeit, also der Aufteilung der Domäne in Subdomänen zu beschäftigen. In der Regel startete man damit, zusammen mit den Anwendern und Fachexperten einen Überblick über die Domäne zu verschaffen. Das kann entweder mit Event-Storming oder mit Domain-Storytelling gemacht werden – zwei Methoden, die für Anwender und Entwickler gleichermaßen gut verständlich sind. In (Abb. 5) ist eine Domain-Story zu sehen, die mit den Anwendern und Entwicklern eines kleinen Programmkinos erstellt wurde. Die grundsätzliche Frage, die bei der Modellierung gestellt wurde, ist: Wer macht was, womit, wozu? Nimmt man diese Frage als Ausgangspunkt, so lässt sich in der Regel sehr schnell ein gemeinsames Verständnis der Domäne erarbeiten.

Überblicks-Domain-Story für ein Programmkino. (Abb. 5)

Als Personen bzw. Rollen oder Gruppen sind in dieser Domain-Story erkennbar: die Werbeagentur, der Kinomanager, der Verleiher, der Kassenmitarbeiter und der Kinobesucher. Die einzelnen Rollen tauschen Dokumente und Informationen aus, wie den Buchungsplan der Werbung, die Vorgaben für Filme und die Verfügbarkeit von Filmen. Sie arbeiten aber auch mit Gegenständen aus ihrer Domäne, die in einem Softwaresystem abgebildet sind: der Wochenplan und der Saalplan. Diese computergestützten Gegenstände sind mit einem gelben Blitz in der Domain-Story markiert. Die Überblicks-Domain-Story beginnt links oben mit der Ziffer 1, wo die Werbeagentur dem Kinomanager den Buchungsplan mit der Werbung mitteilt, und endet bei Ziffer 16, wenn der Kassenmitarbeiter den Saalplan schließt. An diesem Überblick lassen sich verschiedene Indikatoren erklären, die beim Schneiden einer Domäne helfen:

  • Abteilungsgrenzen oder verschiedene Gruppen von Domänenexperten deuten darauf hin, dass die Domain-Story mehrere Subdomänen enthält. In genanntem Beispiel könnte man sich eine Abteilung Kinomanagement und eine Abteilung Kartenverkauf vorstellen (Abb. 6).
  • Werden Schlüsselkonzepte der Domäne von den verschiedenen Rollen unterschiedlich verwendet oder definiert, so deutet dies auf mehrere Subdomänen hin. In dem Beispiel wird das Schlüsselkonzept Wochenplan vom Kinomanager deutlich umfangreicher definiert als der ausgedruckte Wochenplan, den der Kinobesucher zu Gesicht bekommt. Für den Kinomanager enthält der Wochenplan neben den Vorstellungen in den einzelnen Sälen auch die geplante Werbung, den Eisverkauf und die Reinigungskräfte. Diese Informationen sind für den Kinobesucher irrelevant (Abb. 6 – gestrichelte Kreise).
  • Enthält die Überblicks-Domain-Story Teilprozesse, die von verschiedenen Triggern ausgelöst werden und in unterschiedlichen Rhythmen ablaufen, dann könnten diese Teilprozesse eigene Subdomänen bilden (Abb. 6 – durchgezogene Kreise).
  • Gibt es im Überblick Prozessschritte, an denen Information nur in eine Richtung läuft, könnte diese Stelle ein guter Ansatzpunkt für einen Schnitt zwischen zwei Subdomänen sein (Abb. 6 – hellblauer Pfeil).

 

Überblicks-Domain Story mit Subdomänen-Grenzen. (Abb. 6)

Für echte große Anwendungen in Unternehmen sind die Überblicks-Domain-Stories in der Regel deutlich größer und umfassen mehr Schritte. Sogar bei diesem kleinen Programmkino fehlen im Überblick die Eisverkäufer und das Reinigungspersonal, die sicherlich auch mit der Software interagieren werden. Die Indikatoren, nach denen man in seiner Überblicks-Domain-Story suchen muss, gelten allerdings sowohl für kleine als auch für größere Domänen.

 

Übertragung auf den Monolithen

Mit der fachlichen Aufteilung in Subdomänen im Rücken können wir uns nun wieder dem Monolithen und seinen Strukturen zuwenden. Bei dieser Zerlegung werden  Architekturanalyse-Tools eingesetzt, die es erlauben die Architektur im Tool umzubauen und Refactorings zu definieren, die für den echten Umbau des Sourcecodes notwendig sind. Hier eignen sich unterschiedliche Tools: der Sotograph, der Sonargraph, Structure 101, Lattix, Teamscale, Axivion-Bauhaus-Suite und bestimmt noch einige weitere.

 

Mob-Architecting mit dem Team. (Abb. 7)

In (Abb. 7)  sieht man, wie die Zerlegung des Monolithen mit einem Analyse-Tool durchgeführt wird. Die Analyse wird von einem Tool-Pilot, der sich mit dem jeweiligen Tool und der bzw. den eingesetzten Programmiersprachen auskennt, gemeinsam mit allen Architekten und Entwicklern des Systems in einem Workshop durchgeführt. Zu Beginn des Workshops wird der Sourcecode des Systems mit dem Analysewerkzeug geparst (Abb. 7, Nr. 1) und so die vorhandenen Strukturen erfasst (z.B. BuildUnits, Eclipse-VisualStudio-Projekte, Maven-Module, Package-Namespace-Directory-Bäume, Klassen). Auf diese vorhandenen Strukturen werden nun fachliche Module modelliert (Abb. 7, Nr. 2), die der fachlichen Zerlegung entspricht, die mit den Fachexperten entwickelt wurde. Dabei kann das ganze Team sehen, wo die aktuelle Struktur nahe an der fachlichen Zerlegung ist und wo es deutliche Abweichungen gibt. Nun macht sich der Tool-Pilot gemeinsam mit dem Entwicklungsteam auf die Suche nach einfachen Lösungen, wie die vorhandene Struktur durch Refactorings an die fachliche Zerlegung angeglichen werden kann (Abb. 7, Nr. 3) . Diese Refactorings werden gesammelt und priorisiert (Abb. 7, Nr. 4). Manchmal stellen der Tool-Pilot und das Entwicklungsteam in der Diskussion fest, dass die im Sourcecode gewählte Lösung besser oder weitergehend ist, als die fachliche Zerlegung aus den Workshops mit den Anwendern. Manchmal ist aber auch weder die vorhandene Struktur, noch die gewünschte fachliche Zerlegung die beste Lösung und beides muss noch einmal grundsätzlich überdacht werden.

Schön wäre es, wenn eine Zerlegung, wie in (Abb. 8) links dargestellt, möglich wäre. Dort  sieht man die Bounded-Contexts aus dem Kinobeispiel aus (Abb. 6). Auf der linken Seite sind zwei unabhängige fachliche Module zu erkennen, die sich gegenseitig lediglich durch asynchrone Aufrufe über Änderungen informieren. Ansonsten können die beiden Bounded-Contexts ihre Arbeit unabhängig voneinander erledigen. Rechts hingegen sind die User-Interfaces außerhalb der Entity-orientierten Bounded-Contexts untergebracht, damit sie synchrone Aufrufe an den jeweiligen Entity-orientierten Services machen können. Die Konstruktion auf der rechten Seite ist nicht zu empfehlen, weil sie den Big-Ball-of-Mud nicht auseinandernimmt. Sie entspricht auch nicht der von DDD empfohlenen Umsetzung und ist hier nur zur Vermeidung von Missverständnissen dargestellt.

 

 

Bounded Contexts des Kinosystems. (Abb. 8)

 

Das kanonische Domänenmodell

Den größten Knackpunkt bei der Zerlegung stellt in den meisten Monolithen das kanonische Domänenmodell dar. Wenn ich mir große Monolithen anschaue, dann finde ich dort in der Regel ein Domänenmodell, das von allen darauf aufbauenden Teilen der Software verwendet wird. Gut illustrieren lässt sich das an einem System, dass vor einiger Zeit von der Autorin untersucht werden durfte. Die Architekten waren mit dem Wunsch an sie herangetreten, die 1 Millionen Zeilen Java-Sourcecode in Microservices zu zerlegen. Auf die Frage, was für eine Architektur das System habe, wurde keine Schichtenarchitektur, sondern eine Architektur entlang von Use-Cases avisiert. Voller Hoffnung machte man sich mit den Architekten an die Arbeit, die Architektur mit dem häufig eingestehenden Tool-Sotograph[1] zu modellieren und den Sourcecode den modellierten Elementen zuzuordnen.

In (Abb. 9) sieht man dieses System auf der linken Seite, mit seiner ersten Aufteilung in Use-Cases. Jedes Rechteck mailing, importexport usw. beinhaltet einen oder mehrere Package-Bäume mit den darin enthaltenen Klassen. Die Klassen aus den verschiedenen Packages haben Beziehungen zueinander, um ihre jeweiligen Aufgaben zu erfüllen. Diese Beziehungen werden im Sotographen durch die grünen und roten Bögen dargestellt. Die grünen Bögen stellen Beziehungen von oben nach unten dar, die roten Bögen von unten nach oben.

Nachdem mit den Architekten die Use-Cases als Architekturelemente (Rechteck) modelliert wurden, blieb ein großer Teil des Package-Baums übrig, der den Namen model trug. In (Abb. 9) wurde links ein eigenes Architekturelement für model hinzugefügt. In model befinden sich alle Domänenmodellklassen gesammelt an einer Stelle. Dieses „Alles an einer Stelle“ ist erst einmal kein Problem – es wird nur dann zu einem Problem, wenn diese Modellklassen von überallher verwendet werden. Um diesen Umstand zu überprüfen, wurde versucht die Modellklassen aus model den einzelnen Use-Cases zuzuordnen, indem die Klassen von unten nach oben verschoben wurden. Auf der rechten Seite in (Abb. 9) sieht man das vorläufige Ergebnis bis zu calculation.

Architektur mit Use Cases. (Abb. 9)

Rechts in (Abb. 9) sieht man, wie stark das Domänenmodell von überall her verwendet wird und wie stark sich die Domänenmodellklassen untereinander verwenden. In DDD würde man sagen: Das ist ein Big-Ball-of-Mud. Was ist passiert?

Jeder Entwickler, der neue Funktionalität in das System eingebaut hat, hat dafür die zentralen Klassen des Domänenmodells verwendet. Allerdings musste er diese Klassen erweitern, damit er seine neue Funktionalität mit den Domänenmodellklassen umsetzen konnte. So bekamen die zentralen Klassen mit jeder neunen Funktionalität ein bis zwei neue Methoden und Attribute hinzu. Aus dem Blickwinkel der Wiederverwendung von vorhandenen Klassen ist das eine logische Konsequenz. Das Ärgerliche ist nur, dass die Architektur so auf der Ebene des Domänenmodells sehr stark verkoppelt wird.

Domain-Driven-Design geht an dieser Stelle den entgegengesetzten Weg. Der Monolith soll in fachliche Module (Bounded-Contexts) aufgeteilt werden und in jedem Bounded-Context existieren die Klassen des Domänenmodells, die dort benötigt werden. Das bedeutet, dass die Kernklassen des Domänenmodells durchaus mehrfach im System vorkommen werden. Zugeschnitten auf ihren jeweiligen Bounded-Context bieten sie die Methoden an, die dort benötigt werden und nicht mehr.

Will man einen Monolithen fachlich zerlegen, so muss man das kanonische Domänenmodell zerschlagen. Das ist in den meisten großen Monolithen eine Herkulesaufgabe. Zu verwoben sind die auf dem Domänenmodell aufsetzenden Teile des Systems mit den Klassen des Domänenmodells. Um hier weiter zu kommen, werden zuerst die Domänenklassen in jeden Bounded-Context kopiert, der sie braucht (Abb. 10). Der Code wird also dupliziert und dann werden diese Domänenklassen jeweils für ihren Bounded-Context zurückgebaut. So bekommt man kontextspezifische Domänenklassen, die von ihrem jeweiligen Team unabhängig vom Rest des Systems erweitert und angepasst werden können.

Duplizierung der Domänenklassen. (Abb. 10)

Selbstverständlich müssen bestimmte Eigenschaften der Domänenmodellklassen, wie z.B. die ID und der Name, in allen Bounded-Contexts gleich gehalten werden, wo eine Variante einer Domänenklasse vorkommt. Außerdem müssen neue Objekte einer Domänenklasse in allen Bounded-Contexts bekanntgemacht werden, wenn in einem Bounded-Context ein neues Objekt angelegt wird. Diese Informationen werden über Updates von einem Bounded-Context an alle anderen Bounded-Contexts gemeldet, die mit dem jeweiligen Objekt arbeiten.

 

Zusammenfassung

Monolithen sind in den meisten Unternehmen das Ergebnis von 10 bis 15 Jahren Programmierung. Dabei hat meistens die Zeit für Refactorings und Überarbeitung der Architektur gefehlt. Um im eigenen Unternehmen einen Monolithen zu zerlegen, muss zuerst die fachliche Domäne in Subdomänen zerlegt werden und diese Struktur im Anschluss auf den Sourcecode übertragen werden. Das kanonische Domänenmodell muss auseinandergenommen werden, was zum Teil nur durch Code-Duplizierung und anschließendem Zurückbau funktioniert. Folgt man den hier gesammelten Empfehlungen, so kann der Umbau von einem Monolithen zu einer fachlich orientierten Architektur gelingen.

Carola Lilienthal studierte von 1988 bis 1995 Informatik an der Universität Hamburg und promovierte 2008 in Informatik bei Christiane Floyd und Claus Lewerentz an der Universität Hamburg. Dr. Carola Lilienthal ist Geschäftsführerin der WPS – Workplace Solutions GmbH und verantwortet dort den Bereich Softwarearchitektur. Seit 2003 analysiert Dr. Carola Lilienthal in ganz Deutschland Architekturen in Java, C#, C++, ABAP und PHP und berät Entwicklungsteams, wie sie die Langlebigkeit ihrer Softwaresysteme verbessern können. 2015 hat sie ihre Erfahrungen aus über hundert Analysen in dem Buch „Langlebige Softwarearchitekturen“ zusammengefasst. Besonders am Herzen liegt ihr die Ausbildung von Softwarearchitekten, weshalb sie aktives Mitglied bei iSAQB, dem International Software Architecture Quality Board e.V., ist und ihr Wissen regelmäßig auf Konferenzen, in Artikeln und bei Schulungen weitergibt.

 

Literatur

[Ev 2003] „Domain-Driven Design, Tackling Complexity in the Heart of Software“ erschienen bei Addison Wesley

[Li 2017] „Langlebige Softwarearchitekturen – Technische Schulden analysieren, begrenzen und abbauen“ erschienen im dpunkt.verlag, 2017 in der zweiten Auflage.

[1] https://bit.ly/2Ud9kVl

 

Victoria Krautter


Leave a Reply