Angewandte Modularität - Teil 3

Wie funktioniert modulare Software?

Dies ist der dritte Blog zum Thema “Angewandte Modularität” und vorerst letzte mit theoretischem Inhalt.

Der erste Blog machte deutlich, was Modularität in der Softwareentwicklung bedeutet und der zweite, wofür Modularität gut bzw. schlecht ist. Nach dem Was und dem Warum geht es also jetzt um das Wie.

Die meisten Java-Entwickler sind gewohnt, Klassen in verschiedene Packages und Packages in verschiedenen Projekten im Workspace der Entwicklungsumgebung zu verteilen. Die Projekte im Workspace stellen häufig Maven-Projekte dar und Maven baut dann für jedes Projekt ein Artefakt. Sobald eine Klasse C1 aus Projekt A eine andere Klasse C2 aus Projekt B referenziert, muss die Klasse C2 auf dem Klassenpfad von Projekt A gelegt werden. Das geschieht durch eine neue Modulabhängigkeit von Projekt A, indem eine neue Maven-Dependency auf Projekt B in der POM-Datei von Projekt A zugefügt wird. Das alles fühlt sich sehr modular an, ist es aber nicht, denn alle öffentlichen Klassen aus Projekt B können jetzt in Projekt A referenziert werden, obwohl vielleicht nur C2 in Projekt B für den externen Zugriff konzipiert wurde. Alle übrigen Klassen in Projekt B sind nicht vor einem externen Zugriff geschützt. Auf diese Weise bauen wir einen verteilten bzw. einen pseudo-modularen Monolithen, mit einem Abhängigkeitsnetz auf Klassenebene, die über die Modulgrenzen hinweggehen.

Wir kümmern uns nicht darum, ob eine neue Modulabhängigkeit Sinn macht oder nicht. Wir wollen einfach nur unseren Code kompiliert bekommen und hauen deshalb alles auf den Klassenpfad, was dafür notwendig ist und zwar so lange, bis es zu einem Abhängigkeitszyklus kommt, Maven sich weigerte seinen Build durchzuführen und eine so genannte Dependency-Hölle anfängt zu brennen. Es reicht also nicht aus, den Code in verschiedene Artefakte oder Deployment-Units aufzuteilen. Echte Modularität ist mehr, aber was?

Was ist denn ein Modul genau? Im letzten Blog wurde die Unterscheidung von technischem Modul und Geschäftskomponente eingeführt. Wenn man allgemein von Modularität spricht, dann werden Modul und Komponente in der Regel synonym verwendet. Ich werde im Folgenden von einem modularen Artefakt sprechen und damit alle Formen von Modulen und Komponenten einschließen. Ein modulares Artefakt zeichnet sich durch zwei Eigenschaften aus:

  1. Es definiert (typischerweise in einem Modul-Interface), welche Teile seiner Ressourcen von außen zugänglich sind, d.h. welche Klassen von anderen Modulen referenziert werden können.
  2. Es definiert eine Liste von Abhängigkeiten, die benötigt werden, damit der im Modul enthaltene Code funktionieren kann.

Diese beiden Punkte stellen einen Vertrag zwischen einem modularen Artefakt und seiner Laufzeit-Umgebung dar. Dieser Vertrag macht also auf der einen Seite Abhängigkeiten nach außen transparent und schützt auf der anderen Seite vor ungewolltem Zugriff nach innen. Dieser Vertrag ist nicht zu verwechseln mit einer Schnittstellenbeschreibung zweier Module oder Komponenten. Zur Definition einer solchen Schnittstelle gehört nämlich viel mehr, z.B. eine Beschreibung der Datenobjekte, die ausgetauscht werden. Der hier genannte Vertrag bezieht sich auf ein einzelnes modulares Artefakt und seiner Laufzeitumgebung und stellt die Grundlage dar, auf der dann Schnittstellendefinitionen für die Kommunikation zweier Module oder Komponenten aufbauen.

Die oben geschilderten Schwierigkeiten der Pseudomodularität machen deutlich, wie wichtig Punkt 1 aus dem genannten Modul-Vertrag ist: Es muss sichergestellt werden, dass nur der im Vertrag genannte Code aus einem Model von außen sichtbar ist. Module müssen voneinander isoliert werden, sie brauchen eine „Schutzhülle“, die jeden Zugang zu den Innereien eines Moduls (sei er absichtlich oder zufällig) verhindert.

Technisch wird diese Isolierung, Kapselung oder Entkoppelung letztlich durch die Trennung von Klassenpfaden erreicht, aber es gibt darüber hinaus noch eine Reihe von konzeptionellen Möglichkeiten, die sich gegenseitig ergänzen. 2011 stellte Graham Carters der OSGi Community ein “Modularity Maturity Model” vor, dass folgende Stufen beinhaltete: ad hoc –> modules –> modularity –> loose coupling –> devolution –> dynamism. Diese Stufen stellen aus meiner Sicht allerdings eher unabhängige Isolierungsmechanismen dar, die sich dazu eignen Module voneinander abzugrenzen. Aus diesem Grund schlage ich ein Modulisolierungsmodell vor, das folgendermaßen aussieht:

FODM steht für das Fachlich Offene Dependency Management, das in einem späteren Blog noch genauer vorgestellt wird. Keine Modularitätsform unterstützt eine spezielle Form der Code-Verwaltung, aber Micro-Services sind – wie ebenfalls in einem späteren Blog noch deutlich wird – besonders gut für eine verteilte Verwaltung geeignet. Schauen wir uns die Isolierungsmerkmale dieses Isolierungsmodells genauer an:

In einem modularen Artefakt steckt irgendwo die Information, welche Abhängigkeiten zu anderen Modulen bestehen. Entweder muss zur Auflösung der Abhängigkeit ein ganzes Modul zur Verfügung stehen oder nur ein bestimmtes Package  oder sogar nur ein bestimmtes Interface oder eben ein Unified Resource Identifier (URI) bzw. der entsprechende Dienst dahinter. Bei der Definition des Abhängigkeitstyps eines modularen Artefakts kann es möglich sein, eine bestimmte Version anzugeben oder nicht. Das Auflösen der Abhängigkeiten, also das prüfen ob eine Abhängigkeit wirklich zu Verfügung steht, kann während der Entwicklung (also zur Compile-Zeit), während dem Systemstart oder zur Laufzeit stattfinden. Die Dienstvermittlung einer Service-Registry kann statisch (also nur zur Compile-Zeit) oder auch dynamisch (zur Startzeit und zur Laufzeit) erfolgen. Und schließlich kann die Verwaltung des Quellcodes von einem einzigen Entwicklerteam in einem einzelnen Repository erfolgen oder auf verschiedene Teams mit jeweils eigenen Repository aufgeteilt werden.

Je nachdem welche Ausprägung vorliegt, ist ein System von Modulen mehr oder weniger stark gekoppelt. So sind dynamische Systeme weniger gekoppelt als statische. Zentrale Systeme sind stärker gekoppelt als verteilte, Modul-basierte stärker als Package-basiserte oder Interface-basierte. Eine besondere Form, zwei Module voneinander zu isolieren bzw. zu entkoppeln, stellt die Dienstvermittlung dar. Ein Service-Registry erlaubt einem Modul die Registrierung eines Dienstes anhand eines Interface-Typs und ermöglicht einem anderen Modul die Benutzung dieses Dienstes. Wichtig bei dieser Benutzung ist, dass der Dienst im benutzenden Modul nur in Form des Interface-Typs bekannt ist. Der Typ bzw. die Klasse, welche die eigentliche Implementierung hinter dem Interface beinhaltet, ist bei der Benutzung nicht bekannt. Auf diese Weise ist also eine besonders lose Kopplung möglich.

Ziel dieses Blogs war deutlich zu machen, was ein echtes Modul ausmacht und welche Isolierungsmechanismen bestehen, um den Quellcode zweier Module sauber zu trennen. Damit ist der theoretische Teil in der Blogserie “Angewandte Modularität” abgeschlossen und im nächsten Blog werde ich eine willkürliche, aber typische Fachdomäne vorstellen, die in den weiteren Blogs benutzt wird, um verschiedene Formen der Modularität vorzustellen und zu vergleichen.

 


Jetzt teilen: