Microservices und ihre Alternativen
Code-Monolithen
Das Ganze macht aber noch nicht unbedingt ein Anti-Pattern, sondern soll zunächst nur die Nachteile des Architekturmusters aufzeigen. Die Erklärungen deuten dabei aber bereits an, wie fliessend der Übergang zwischen Pattern und Anti-Pattern sein kann (vgl. Kasten Pattern versus Anti-Pattern). Einen Fall, in dem deutlichere Zeichen eines Anti-Patterns zu erkennen sind, wollen wir nachträglich als Sourcecode-Monolithen bezeichnen. In diesem Fall findet sich der gesamte Code der Applikation in einer einzelnen Codebasis wieder, auf der auch alle Entwickler zeitgleich und ohne grössere Abgrenzung arbeiten und die intern so stark verwoben ist, dass man Bestandteile nur sehr schwer aus ihr herauslösen kann.
Zugegeben, sowohl Microsoft als auch Google folgen einem ähnlichen Vorgehen für sehr grosse Softwareprojekte, und der Begriff des Mono-Repos (Monolithic Repository) beschreibt ein sehr ähnliches Vorgehen, das gerade in der Webentwicklung einigen positiven Zuspruch erhält. Hierbei arbeiten dann verschiedene Teams an verschiedenen Bestandteilen eines verteilten Softwaresystems, teilen sich aber ein grosses Code-Repository. Dies hat zum Beispiel den Vorteil, dass der gesamte Code zeitgleich zur Verfügung steht und man «mal eben nachschauen» kann, wie denn eine Methode konkret umgesetzt ist, die man verwenden will.
Pattern versus Anti-Pattern
Das Verhältnis zwischen Pattern und Anti-Pattern ist scheinbar sehr eindeutig, die Klassifizierung aber teils sehr schwer. Eindeutig ist hierbei zunächst nur, dass Patterns etwas sind, das uns die Erläuterung komplexer Zusammenhänge dank einer eindeutigen Namensgebung erleichtert. Ein echtes Muster ergibt sich aber nicht nur anhand des Namens, sondern auch aufgrund der wiederkehrenden Natur der Zusammenhänge. So weiss man bei einem Singleton sofort, dass es sich um eine Klasse handelt, die nur eine Instanz haben kann. Dieser Umstand ist sehr oft anzutreffen und wird somit auch oft von Entwicklern wahrgenommen und verstanden. Indem man der Sache nun einen Namen und eine Beschreibung gibt, kann man ausserdem mit ihnen Best Practices verbinden.
Wie genau soll also ein Singleton umgesetzt werden, damit man Probleme vermeidet? Das Pattern an sich ist zunächst also wertungsfrei, auch wenn Patterns generell gern positiv konnotiert sind. So muss ein Singleton nicht in jedem Fall eine gute Idee sein, andernfalls würden wir ja nur noch mit dieser Art von Klassen arbeiten. Es wird erst aufgrund seines Kontexts zu einer guten Entscheidung, und Musterbeschreibungen wie das berühmte Entwurfsmusterbuch der Gang of Four liefern deshalb den Kontext für Patterns auch mit.
Bei einem Anti-Pattern ist es ganz ähnlich. Auch dies ist zunächst ein wiederkehrendes Muster mit einem eindeutigen Namen. Hierzu zählt zum Beispiel der berühmte Spaghetti-Code, den man nur schwer verstehen kann, weil keine eindeutigen Verantwortlichkeiten zu erkennen sind. Anti-Patterns sind dabei aber grundsätzlich negativ konnotiert. Etwas als Anti-Pattern zu bezeichnen ist also so, als würde man es mit einem grossen Schild «Warnung» versehen.
Tatsächlich ergibt sich die Schadenswirkung eines Anti-Patterns aber ebenfalls erst aus seinem Kontext. Dies wird beispielsweise bei Code-Clones deutlich. Hierbei handelt es sich um Codeabschnitte, die sich sehr stark ähneln und zum Beispiel durch Copy-and-paste entstehen können. Sie sind schädlich und sollten vermieden werden, weil sie gegebenenfalls die gleiche Logik an verschiedenen Stellen in der Codebasis duplizieren. Nun ist aber nicht jede Zeile Code, die einer anderen ähnelt, auch gleich ein Klon. Vielmehr kann sich Code auch einfach so ähneln, weil er den gleichen Aufbau hat, nicht aber weil er die gleiche Logik abdeckt. Teilweise wird sogar bewusst Code kopiert, um keine unnötigen Abhängigkeiten herbeizuführen.
Ob ein Muster also hilfreich oder gar schädlich ist, ergibt sich aus seinem Kontext, und demzufolge sollte dieser Kontext vor dem Einsatz ganz klar geprüft werden. Dann kann sich auch schon mal herausstellen, dass ein offensichtlich scheinendes Anti-Pattern nur das geringste aller sonst möglichen Übel ist.
Sind in diesem Zusammenhang aber keine organisatorischen oder technischen Grenzen etabliert, hängt es nur noch von der Disziplin des Entwicklers ab, ob er jene Methode dann nicht auch noch gleich «anpasst». Das «mal eben etwas nachschauen» wird somit sehr leicht zum «mal eben etwas ändern». Solch ungeplante Spontanänderungen leisten der Architekturerosion aber Vorschub, indem sie Codebestandteile verbinden, die eigentlich entkoppelt sein sollten, was in der Praxis meist durch ein Umgehen der Schichtentrennung wahrgenommen werden kann. Sie sorgen ausserdem für Bugs, da der Entwickler nicht immer wissen kann, in welchem Kontext der von ihm geänderte Code noch verwendet wird, und sie machen den Code insgesamt schwerer verständlich und lösen schnell eine Kaskade weiterer ungeplanter Änderungen aus.
Zugegeben, dies klingt, als würde der Autor versuchen, ein Horrorszenario zu konstruieren. Aber gerade in sehr lang laufenden Projekten, mit sehr grosser Codebasis, ohne Code-Reviews und statischer Codeanalyse, ergibt sich immer das gleiche Bild. Häufig kommt es zu einer Wucherung innerhalb des Quellcodes, die nachträglich nur sehr schwer zu beheben ist. Als Ergebnis kann die Codebasis dann nicht mehr aufgetrennt werden, weil die verschiedenen Bestandteile so stark miteinander verbunden sind, dass sie nur noch als Ganzes funktionieren. Trennschichten haben sich damit also aufgelöst, und Schnittstellen sind nur noch Makulatur. Der ganze Prozess verläuft dabei so schleichend, dass er erst wahrgenommen wird, wenn es schon zu spät ist (vgl. Kasten Architekturerosion).
Um nun aber keinen falschen Eindruck zu erwecken: Ein Mono-Repo ist nicht zwangsläufig ein Anti-Pattern. Zum Code-Monolithen wird es, weil es unzureichende organisatorische und technische Sicherungsmechanismen gibt, um besagten Wildwuchs zu verhindern.
Architekturerosion
Architekturerosion beschreibt, inwieweit sich die bestehenden Strukturen eines Softwaresystems von den Strukturen unterscheiden, die tatsächlich gebraucht werden beziehungsweise geplant sind. Der Prozess der Architekturerosion geschieht dabei meist schleichend über einen längeren Zeitraum hinweg und wird beschleunigt durch unzureichendes Requirements Engineering, einen hohen Zeitdruck bei der Implementierung, Unerfahrenheit der Projektbeteiligten und ein unzureichendes Bewusstsein für die innere Qualität von Software.
Als Entwickler nimmt man Architekturerosion meist in Form von technischen Schulden wahr. Personen, die nicht direkt an der Entwicklung beteiligt sind, erleben sie durch wiederkehrende Fehler, erheblichen Mehraufwand bei Änderungen und sinkende Gesamtproduktivität des Entwicklungsteams.
Da Architekturerosion meist sehr langsam vonstattengeht, sich langfristig aber verheerend auswirken kann, stellen Personen ausserhalb des Entwicklungsteams deren Schweregrad meist erst fest, wenn die Software mit erheblichen Wartungskosten verbunden ist und die Architektur nur über entsprechend hohe Investitionen in umfassende Restrukturierungen wiederhergestellt werden kann. Aus diesem Grund sollten die Strukturen von Software bereits von Entwicklungsbeginn an durch entsprechende Codeanalysen und automatisierte Tests gegen Erosion abgesichert werden, und es sollte kontinuierlich in ihren Werterhalt investiert werden.
Warum macht man so was (nicht)?
Es stellt sich also die Frage, warum man nicht von Beginn an die Software entsprechend aufteilt und wartbar gestaltet. Bei neuer Software kann und sollte man dies auch tun. Bei bestehender Software hat dies meist den einfachen Grund, dass natürlich gewachsene Software nun einmal genau so entsteht. Mit «natürlich gewachsen» ist hierbei gemeint, dass die Software ohne grössere Anpassungen der Gesamtarchitektur immer weiterentwickelt wird und die Architekturerosion somit ungehindert fortschreiten kann. Man beginnt mit einem kleinen Programm, das bestimmte Aufgaben erfüllt, und diese Aufgabenmenge steigt über die Jahre hinweg, womit die internen Strukturen des Softwaresystems selbst auch immer komplexer werden. Hierbei nachträglich eigenständige Module oder Komponenten herauszulösen, diese separiert bereitzustellen und zu pflegen ist mit einem erheblichen Restrukturierungsaufwand verbunden, der den Stakeholdern meist nur schwer vermittelt werden kann.
Die Nachteile von Monolithen liegen also auf der Hand: Sie können nur beschränkt skaliert werden und verleiten zu schwer wartbaren Strukturen. Somit wundert man sich nicht, dass eine Alternative, die all diese Nachteile adressiert, auf so offene Ohren gestossen ist wie Microservices.
Autor(in)
Hendrik
Lösch