Der Vorteil von klassischen Monolithen ist, das alles, was irgendwie zum System gehört, mit an Board ist, d.h. alles, was an einer beliebigen Codestelle benötigt wird, ist ohne Zusatzaufwand erreichbar. Abhängigkeitsprobleme gibt es nicht, weil Klassen sich gegenseitig referenzieren können. Für kleine Programme und Wegwerf-Prototypen ist eine monolithische Struktur genau das Richtige.
Eine Zwischenform von Monolith und modularer Software stellt der verteilte oder pseudomodulare Monolith dar. Auf dem Weg zu echter Modularität stellt er einen evolutiven Zwischenschritt dar, der für eine etwas erhöhte Komplexität ausreichend ist, aber das Prinzip “Teile und Herrsche” nur oberflächlich verwirklicht.
Wenn die Anforderungen an eine Anwendung steigen, die Codebase wächst und die Software komplexer wird, stellen sich beim Monolithen Probleme ein, die in den ersten drei Blogs dieser Serie beschrieben sind.
Die Komplexität ist das grundlegendste und letztlich größte Problem in der Softwareentwicklung und diese wächst stetig, weil der Markt (also wir alle) immer mehr von unserer Software erwarten. Immer mehr Daten sollen in immer kürzerer Zeit auf immer noch komplexerer Weise verarbeitet werden.
Das wirkungsvollste Mittel dagegen ist die Modularität, also die Anwendung des Prinzips “Teile und herrsche”. Für die Codebase bedeutet das, den Code in möglichst unabhängige Teile (Komponenten, Module) aufzuteilen. Genau dazu dient der in Blog 3 beschriebene Modulvertrag.
Bereits vor über 10 Jahren wurde mit der ersten Version von OSGi der Modulvertag in der Java-Welt spezifiziert. OSGi lädt dazu ein, feingranulare Module mit hohem Wiederverwendungswert zu bauen. Die auf OSGi basierende Eclipse IDE zeigt, wie hunderte oder gar tausende von Modulen (dort Plugins genannt) im Prinzip wie ein Legosystem zu verschiedenen Anwendungen zusammengebaut werden können und wie die Endanwender weitere Plugins ihrer individuellen Wahl einbinden können, um ihre Zwecke zu verfolgen.
Eine andere bereits schon in die Jahre gekommene Modularitätsform ist SOA (Service Oriented Architecture). Allerdings ist ihre modernere Variante, die Microservices Architektur mittlerweile viel verbreiteter. Der Vorteil von Microservices gegenüber OSGi sind die Skalierungsmöglichkeiten und Ausfallsicherheiten. Performance-Engpässe können durch Vervielfachung von Runtime-Containern für bestimmte kritische Services relativ einfach überwunden werden und so auch die Resilienz drastisch gesteigert werden. Allerdings werden in der Microservices Architektur die Module (d.h. die Services) nur horizontal, d.h. fachlich geschnitten. Bei OSGi zusätzlich auch vertical (also technisch). Auf diese Weise entstehen im OSGi-Umfeld viel kleinere Bausteine, die ein Lego-Baukastensystem (“Legoware”) ermöglichen.
OSGi und Microservices haben aber einen gemeinsamen Nachteil. Es braucht viel Know-how und Erfahrung, diese Modularitätsformen effektiv anzuwenden. Die Lernkurve ist sehr anstrengend und die Implementierung neuer Modulverträge und Schnittstellen zwischen ihnen aufwändig. Viel leichtgewichtiger ist das fachlich offene Dependency Management, das mit einfachsten Bordmitteln auskommt und zusätzlich eine Dependency-Hölle, wie sie bei OSGi oder beim verteilten Monolithen ab einer bestimmten Komplexität sehr wahrscheinlich ist, ausschließt. Das fachliche offene Dependency Management führt zu einem modularen Monolithen. Die Software stellt nach außen einen großen Block dar, der immer nur als ein einzelnes großes Artefakt deployt wird. Innen jedoch besteht er aus gut gekapselten Komponenten. Damit lassen sich manche Vorteile des Monolithen mit den Vorteilen der Modularität verbinden und das ohne aufwändige Hilfsmittel, deren Einsatz mühsam zu erlernen ist.
Eine andere Modularitätsform, die zu einem modularen Monolithen führt, ist das Modulsystem von Java9. Es erlaubt zwar wie OSGi keine zyklischen und wechselseitigen Abhängigkeiten, aber es ist recht leichtgewichtig. Einerseits, weil es direkt in die Programmiersprache eingebettet ist und andererseits auch, weil es viel einfacher (und damit natürlich auch leistungsschwächer) als OSGi ist. Beispielsweise können in OSGi versionierte Abhängigkeiten (auch mit halboffenem oder geschlossenem Range) definiert werden oder auch einzelne Packages als Abhängigkeit angegeben werden. Mit den Java9Moduls geht das nicht, aber dafür ist ihre Handhabung einfach, überschaubarer und ziemlich schnell erlernbar.
Einige Modularitätsformen lassen sich auch kombinieren. So könnte ein einzelner Microservice OSGi-basiert sein oder einen modularen Monolithen darstellen. Es lässt sich auch das fachliche offene Dependency Management mit Java9Modules realisieren.
Die verschiedenen Ansätze, echte Modularität zu realisieren, haben alle ihre Vor- und Nachteile, die ich in “Applied Modularity” ausführlich darstelle. Hier möchte ich mich darauf beschränken, dass jede der vorgestellten Modularitätsformen ihr spezifisches Anwendungsgebiet hat und bei den entsprechenden Anforderungen das Mittel der Wahl sein sollte. Folgende Tabelle fasst das zusammen:
OSGi: Legoware wie z.B. die Eclipse IDE
Microservice: Cloud-Services mit großer Skalierbarkeit und maximaler Verfügbarkeit
Modulare Monolithen: Geschäftsanwendungen für die Wiederverwendung von Bauteilen nicht entscheidend und eine Nichtverfügbarkeit außerhalb von Geschäftszeiten unproblematisch ist.