Angewandte Modularität - Teil 6

Modularität in Java ohne Frameworks

Im vorigen Blog zur Serie „Angewandte Modularität“ hatte ich drei HOO-Implementierungen vorgestellt, nämlich die des verteilten Monolithen, die OSGi-Implementierung und die Java9Modules-Version. In diesem Blog möchte ich zwei weitere Implementierungen vorstellen, die auch wieder die HOO-Fachdomäne nutzen.

Zunächst widme ich mich der Microservices-Architektur.

Bei dem Begriff „Microservices“ gehen Meinungen häufig auseinander. Das habe ich Kapitel 4.1 in „Applied Modularity“ ausführlich beschrieben. Allgemein verstehe ich darunter einen Architekturstil, der den drei folgenden Prinzipien folgt:

  1. Explizite Grenzen zwischen den Services,
  2. Services teilen sich nur ihren Vertrag keine Klassen,
  3. Unabhängige Entwicklung und unabhängiges Deployment.

Der grundlegendste Unterschied von Mircoservices im Vergleich mit allen anderen Modularitätsformen ist, dass jeder Service in einer eigenen Runtime-Umgebung läuft. D.h. für Java, dass jeder Service in seiner eigenen JVM läuft. Auf diese Weise erfüllen Mircoservices die Anforderung, unabhängig deployt werden zu können. Dieser Unterschied hat weitreichende Auswirkungen. Eine davon ist, dass eine Klasse in Service A keinerlei Klassen in anderen Services referenzieren kann, weil diese außerhalb ihrer eigenen Runtime liegen. Klassen von Objekten, die über eine Schnittstelle zwischen den Services ausgetauscht werden, müssen in beiden kommunizierenden Services vorhanden sein. Die zu kommunizierenden Daten müssen daher in Service A von einem Java-Datenobjekt in eine übermittelbare Form umgewandelt werden und von Service B wieder zurück in ein Java-Objekt überführt werden (falls Service B ebenfalls Java basiert ist). Eine typische Form für die Übermittlung von Daten über Service-Schnittstellen sind XML-Daten via REST-Schnittstellen. Für den Verbindungsaufbau muss ein Service A eine URI kennen unter der Service B eine nach außen zur Verfügung gestellte Service-Methode (sogenannter Endpoint) anbietet. Diese Form der Kommunikation (XML via REST) findet sich auch in der Implementierung der HOO-Fachlichkeit als Microservices-System. Wie damit die HOO-Fachdomäne als Microservices-System aussieht, lässt sich hier begutachten: https://github.com/iks-github/DemoCode/tree/master/HOroscopeOnlineService/orchestration/hoo-micro-services-o. Wie man diesen Code in der IDE zum Laufen bekommt ist hier erklärt: https://github.com/iks-github/DemoCode/wiki/HOO-Micro-Services.

Betrachtet man die Abhängigkeiten in dieser HOO-Implementierung, kann man die Maven-Dependencies in der pom.xml und die referenzierten URIs im Code betrachten.

Maven Dependencies

Die Services kennen sich auf Ebene der Maven-Dependency nicht, verwenden aber eine gemeinsame Bibliothek (ApiUtils), die zur Erhöhung der Konformität verwendet wird. Die Codebases der Services sind also vollständig getrennt (nutzen nur eine gemeinsame Bibliothek). Die URI-Calls machen nun die fachlichen Abhängigkeiten zwischen den Modulen bzw. Services sichtbar. Die Summe aller URI-Calls eines Services definiert also die Requirements eines Moduls in einer Microservice-Architektur. Die Endpoints eines Services, auf welche die URI-Calls von außen zielen, definieren, welche Teile eines Moduls (eines Services) von außen sichtbar sind. Auf diese Weise ist der Modulvertrag bei Microservices spezifiziert.

Eine weitere Auswirkung der getrennten Runtime-Umgebungen ist, dass wechselseitige und zyklische Aufrufe von Services möglich werden. Service A (z.B. der Horoscope-Service) könnte einen Endpoint von Service B (z.B. Order-Service) verwenden und gleichzeitig auch umgekehrt. Im nächsten Blog werden wir sehen, welche Bedeutung dies hat.

Echte Modularität ist aber auch mit ganz einfachen Mitteln möglich, d.h. ganz ohne Framework wie OSGi oder mit verschiedenen Runtimes und Schnittstellenbeschreibungen wie den Microservices. Und das bei Bedarf auch mit wechselseitigen und zyklischen Aufrufen (d.h. Abhängigkeiten). Zu diesem Zweck ist ein Dependency-Management mit einem bestimmten Design nötig. Das fachlich offene Dependency Management verfolgt genau diesen Zweck. Um die Intention hinter dieser Modularitätsform zu verstehen, muss zwischen fachlicher und technischer Abhängigkeit unterschieden werden. Technische Abhängigkeiten sind z.B. Dependencies, die als Maven-Dependency in der pom.xml definiert sind. Fachliche Abhängigkeiten ergeben sich aus der Geschäftslogik der Fachdomäne, z.B. muss das Control-Modul das Horoskop-Modul aufrufen, um das Horoskop für einen Auftrag zu bekommen. In den Modularitätsformen, die ich im letzten Blog vorgestellt habe (verteilter Monolith, OSGi und Java9Modules) wurden alle fachlichen Abhängigkeiten durch technische abgebildet. Wie oben die Microservice Architektur und jetzt auch das Fachliche Offene Dependency Management zeigen, muss das aber nicht sein.

Die Grundidee des fachliche offenen Dependency Management ist, dass es keine technischen Abhängigkeiten unter den Komponenten gibt, deren Bildung rein fachlich motiviert ist. Diese Komponenten implementieren einen fachlich abgrenzbaren Teilkontext (z.B. Auftragsverwaltung). Neben diesen fachlichen Komponenten kann es rein technisch motivierte Komponenten geben, von denen die fachlichen Komponenten abhängig sein können. Es dürfen aber keine fachlichen Abhängigkeiten in der Geschäftslogik (z.B. zwischen Auftragsverwaltung und Abrechnung) über eine technische Abhängigkeit realisiert werden. Um das zu vermeiden gibt es eine API-Komponente, die alle fachlichen Interfaces beinhaltet. Fachliche Interfaces ergeben sich aus der Anwendung von Domain Driven Design (DDD). Für jeden sogenannten Bounded Context innerhalb der Fachdomäne gibt es eine Komponente, die einen bestimmten fachlichen  Bereich (z.B. Auftragsverwaltung) implementiert. Die Service-Methoden (d.h. die Funktionalität), die eine solche Komponente zur Nutzung anderen Komponenten anbietet, werden in einem oder mehreren Java-Interface beschrieben. Die Interfaces aus allen Komponenten werden zu einer zentralen API-Komponente zusammengefasst und die fachlichen Komponenten sind dann technisch von dieser API-Komponente abhängig, aber nicht untereinander. Die API-Komponente enthält also die Information, welche Teile einer fachlichen Komponente von außen sichtbar sind. Innerhalb einer fachlichen Komponente definieren die Referenzen von fremden Interfaces die Requirements dieser Komponente. Auf diese Weise ist der Modulvertrag im fachlich offenen Dependency Management spezifiziert.

Wie das für die HOO-Fachdomäne im fachlich offenen Dependency Management aussieht, lässt sich hier begutachten: https://github.com/iks-github/DemoCode/tree/master/HOroscopeOnlineService/orchestration/hoo-open-domain-dependency-management-o. Wie man diesen Code in der IDE zum Laufen bekommt ist hier erklärt: https://github.com/iks-github/DemoCode/wiki/HOO-Open-Domain-Dependency-Management.

Die technischen Abhängigkeiten dieser Implementierung sehen folgendermaßen aus:

Angewandte Modularität-technische Anhängigkeiten

Die Control-Komponente hat hier einen hybriden Charakter und stellt eine teils fachlich teils technische motivierte Komponente dar, weil sie einerseits einen wesentlichen Teil der Geschäftslogik beinhaltet, andererseits technische Abhängigkeiten zu den übrigen fachlichen Komponenten aufweist. Dies ist nötig, weil die Control-Komponente hier einen Dependency Injection Container darstellt, der die Instanzen aller Services aller Komponenten erzeugt und mit ihrem Interface-Typ dort injiziert, wo sie gebraucht werden (z.B. den Order-Service im Horoscope-Service). Da die Control-Komponente grundsätzlich alle fachlichen Komponenten kennt, die übrigen fachlichen Komponenten untereinander aber nicht, könnten wie bei den Microservices beliebige wechselseitige und zyklische fachliche Abhängigkeiten implementiert werden. Technisch muss keine neue Abhängigkeit zwischen den Komponenten definiert werden, weil alles Nötige über die API-Komponente zur Verfügung gestellt wird. Eine Dependency-Hölle ist somit unmöglich. Nachträglich können beliebige fachliche Abhängigkeiten schmerzfrei ein- oder umgebaut werden. Auch komplett neue Komponenten mit beliebigen Abhängigkeiten versuchen keinerlei Dependency-Wirrwarr.

Im nächsten Blog werde ich deutlich machen, welchen Unterschied es macht, zyklische und wechselseitige Abhängigkeiten gezielt einsetzen zu können.


Jetzt teilen: