Warum modular und wann besser nicht? (Angewandte Modularität, Teil 2)

Dies ist der zweite Blog zum Thema „Angewandte Modularität“. Der erste Blog machte deutlich, was Modularität in der Softwareentwicklung bedeutet. Dort wurde bereits zum Ausdruck gebracht, dass Modularität der Wartbarkeit von komplexer Software dient. Diese allgemeine Aussage soll vertieft werden.

Sinnvoll modularisierte Software ist ausgesprochen verständlich. Im letzten Blog hatte ich eine Softwareanwendung mit einem Lehrbuch verglichen (Anweisungen sind Sätze, Packages sind Kapitel usw.). Je kleiner diese Strukturebenen geschnitten sind und je präziser jede einzelne Einheit in den verschiedenen Ebenen benannt wurde, desto verständlicher ist auch die Software. Die Verständlichkeit ist besonders wichtig vor dem Hintergrund, dass es nur zu den wenigsten Softwareprodukten gute technische Dokumentationen gibt und die Wartbarkeit von Software fast immer nur von dem Wissen in den Köpfen der Entwickler abhängt. Wenn diese nicht mehr zur Verfügung stehen (Krankheit, Urlaub, Arbeitsplatzwechsel), ist eine komplexe monolithische Software kaum noch wartbar.

Gleiches gilt für die Änderbarkeit. In einem großen Geflecht von Quellcode-Dateien (in Java-Klassen, die sich gegenseitig referenzieren) sind viele Änderungen am Code praktisch nicht mehr möglich, weil es zu viele Auswirkungen auf andere Klassen hat. Im Falle einzelner kleinerer Module, die nur aus wenigen Quellcode-Dateien (Klassen) bestehen, kann im Bedarfsfall ein Modul problemlos umgebaut werden, ohne dass andere Module davon betroffen sind. Das soll folgende Abbildungen illustrieren:


Ein dritter Aspekt ist die Testbarkeit. Diese stellt die Softwarequalität während der Entwicklung sicher. Das geschieht typischerweise durch automatisierte Tests, die einzelne kleine Code-Abschnitte ausführen und prüfen, ob das Ergebnis der Erwartung entspricht (so genannte Unit-Tests). Mittels der im Test Driven Development  genannten Methode lassen sich diese Tests parallel zur Entwicklung des Quellcodes der eigentlichen Anwendung bequem mitentwickeln und eine hohe Testabdeckung erreichen. Wenn die Kopplung zwischen den Klassen sehr hoch und die Kohärenz sehr gering ist, ist der Aufwand für solche Tests allerdings trotz Einsatz von Mock-Frameworks wie Mockito recht hoch.

Daneben gibt es die Systemtests, die eine komplette Anwendung testen und einen großen Ausschnitt ihrer Funktionalität überprüft. Diese Tests zeichnen sich durch eine große Testtiefe aus, da von der Oberfläche über die Programmlogik bis zur Datenbank alle Softwareteile im Verbund getestet werden.

Außer den Quellcode-nahen Unit-Tests, die nur kleinere Codeabschnitte testen, und den Quellcode-fremden Systemtests, die das Große und Ganze testen, können wir Integrationstests schreiben, die eine größere Funktionalität testen, die aus mehreren Bausteinen zusammengesetzt ist, ohne dass das ganzes System zur Verfügung stehen muss. Bei hinreichend komplexer Software ist die Qualitätssicherung (und damit die Wartbarkeit) ohne diese Zwischenebene kaum möglich. In einem großen Geflecht von Klassen ist es aber häufig praktisch unmöglich eine Gruppe von Klassen so zu isolieren, dass eine bestimmte Funktion der Software unabhängig von anderen Funktionen getestet werden kann. Ohne Module fehlt also ganz praktisch der Scope für Integrationstests. Die Testbarkeit von klassischen Monolithen, die aus einem riesigen Netz von Abhängigkeiten bestehen, ist damit sehr stark eingegrenzt.

Der Vorteil von modularer Software betrifft aber nicht nur die Arbeit der Softwareentwickler, sondern er ist von ganzheitlicher Natur, denn er betrifft auch andere Phasen des Application Lifecycle Management (siehe auch http://clean-coding-cosmos.de/der-entwicklerkosmos/sep-alm-1/sep-alm-2), nämlich die Anforderungsanalyse und den Betrieb der Software. Bereits bei der Anforderungsanalyse ist sowohl ein tief greifendes Verständnis als auch ein großer Überblick ohne sinnvolle Unterteilung der ganzen Fachdomäne kaum möglich. Aus diesem Grund ist auch die im Domain Driven Design genannte Vorgehensweise völlig berechtigt und zurzeit in aller Munde.

Beim Betrieb von modularer Software ist es möglich, nur einzelne Teile auszutauschen, die ein Update benötigen. Das erhöht die Verfügbarkeit. Manche Komponenten können einen Performanz-Engpass darstellen, der behoben werden kann, indem genau diese Komponenten auf mehrere Runtime-Container verteilt werden und so die Last ausgleichen können (Skalierbarkeit).

All diese Vorteile von Modularität gibt es aber nicht umsonst. Wie in den kommenden Blogs noch deutlich werden wird, gibt es Modularität nicht umsonst und bestimmte Formen von ihr kosten sehr viel. Das liegt letztlich daran, dass Modularität keine Komplexität reduzieren kann – zumindest nicht die inhärente Komplexität der Fachdomäne und deren Anforderungen an die Software. Natürlich kann man durch schlechtes Design unnötige Komplexität einem System hinzufügen – aber die inhärente Komplexität eines Systems lässt sich nicht verringern, sondern nur geschickt verteilen, sodass das System verständlich, änderbar und testbar bleibt. Vernünftigerweise verteilt man die inhärente Komplexität auf verschiedenen Ebenen, d.h. auf Architektur-, Design-, und Implementierungsebene. Wenn ein System wächst, ist es sinnvoll, es in verschiedene Subsysteme zu zerlegen. Wenn ein Subsystem wächst, ist es sinnvoll, es in verschiedene Komponenten zu zerlegen. Wenn eine Komponente wächst, ist es sinnvoll, es in verschiedene Module zu unterteilen. Wenn ein Modul wächst, ist es sinnvoll, es in einen sinnvollen Baum von Packages und Klassen aufzuteilen. Der Grad dieser Aufteilung sollte der inhärenten Komplexität der Fachdomäne entsprechen. Egal, ob man auf der Architektur-, Design- oder Implementierungsebene unterwegs ist, stets ist viel Erfahrung nötig, um eine sinnvolle Unterteilung zu finden. Ungünstige Unterteilungen und falsch verstandene Modularisierung führen zu unnötiger Komplexität und können in Form eines „verteilten Monolithen“ ein größeres Problem darstellen als ein klassischer Monolith.

In einem späteren Blog, nachdem die verschiedenen Ansätze zur Modularisierung vorgestellt wurden und diese verglichen werden können, wird deutlich werden, dass z.B. Micro-Services eine recht aufwendige und teure Form von Modularisierung ist, deren Anwendung durch eine hohe Priorisierung von Verfügbarkeit und Skalierung sehr gut gerechtfertigt sein sollte. Außerdem wird deutlich werden, dass in vielen Fällen so etwas wie ein „Modularer Monolith“ mit relativ einfachen Mitteln entwickelt und gewartet werden kann.

Dieser Blog hatte das Ziel die Vor- und Nachteile von Modularität deutlich zu machen. Mehr Informationen dazu gibt es in dem Buch „Applied Modularity“ (https://leanpub.com/applied-modularity). Der nächste Blog in der Reihe „Angewandte Modularität“ beschäftigt sich mit den Mitteln, mit welchen Modularität erreicht werden kann.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.