Über unsMediaKontaktImpressum
Martin Lehmann & Dr. Kristine Schaal 19. Juni 2018

Modularity Patterns mit dem Java-Modulsystem Jigsaw

Seit Java 9 steht mit dem neuen Modulsystem Jigsaw ein natives Sprachmittel zur Verfügung, mit dem man in Java Komponenten definieren und in der Architektur verankern kann. Darauf hat die Java-Community lange gewartet. Hält Jigsaw das Verspechen, damit eine bessere, modulare Software-Architektur erstellen zu können? Wie sehen Komponenten mit Jigsaw konkret aus, welche typischen Patterns kann man damit umsetzen? In diesem Artikel betrachten wir bekannte Modularity Design Patterns und zeigen, ob und wie man diese mit Jigsaw umsetzen kann.

Definition von Komponenten

Komponenten sind eigentlich nichts wirklich Neues. Die Idee von "Komponentenorientierter Softwareentwicklung" ist seit vielen Jahren ein fester Bestandteil im Software-Architektur-Entwurf. Verschiedene Definitionen [1,2] legen den Komponentenbegriff fest. Auch wenn sich die Definitionen im Detail leicht unterscheiden, besagen sie insbesondere:

  • Eine Komponente ist ein Software-Baustein und umfasst als grobgranulare Einheit eine Reihe von Klassen und Packages.
  • Eine Komponente hat explizite Abhängigkeiten zu anderen Komponenten. Diese sind als eingehende und ausgehende Schnittstellen modelliert.
  • Eine Komponente ist selbständig deploybar.

Vorteile

Mit Komponenten bricht man im Software-Entwurf große Monolithen in kleinere, bessere managebare Teile auf. Durch die explizite Modellierung von Abhängigkeiten über die Schnittstellen wird nachvollziehbar, was bei Änderungen passiert und welche Teile von Änderungen überhaupt betroffen sind. Ändert sich eine Komponente, so hat das nur Auswirkungen auf die abhängigen Komponenten. Der Grad der Abhängigkeiten der Komponenten bestimmt die Komplexität der Anwendung maßgeblich.

Ein weiterer Vorteil einer solcher expliziten Modellierung von Abhängigkeiten ist, dass zur Laufzeit nur das geladen werden muss, was wirklich benötigt wird. Damit wird der "Footprint" i.d.R. deutlich kleiner. Umgekehrt kann man entlang der Abhängigkeitsbeziehungen schnell erkennen, ob und welche Teile fehlen und so Laufzeitfehler möglichst früh erzeugen, idealerweise schon beim Start der Anwendung.

Design

Insbesondere legen zwei Charakteristika fest, wie Komponenten im Software-Entwurf zu schneiden sind:

  1. Hohe Kohäsion als hohe innere "Bindung" innerhalb einer Komponente: Der fachliche oder technische Zusammenhalt der Bestandteile einer Komponente (Klassen etc.) ist sehr stark ausgeprägt.
  2. Lose Kopplung: Zwischen Komponenten sollen Beziehungen nur über wenige, "dünne" Schnittstellen erfolgen. Änderungen an einer Komponente sollen sich möglichst nur lokal auswirken und andere Komponenten möglichst wenig beeinflussen.

An diesen beiden Grundsätzen kann man sich (zumindest analytisch) orientieren und seinen Komponentenschnitt überprüfen. Komponenten sollten eine angemessene Größe haben, in der Regel sind weder zu wenige (nur zwei oder drei) große Komponenten sinnvoll, noch zu viele (hunderte oder tausende) kleine.

Grundlagen und Ziele von Jigsaw

Das "Project Jigsaw" für das Java-Modulsystem ist im Java Specification Request (JSR) 376 spezifiziert. Der JSR 376 umfasst eine Reihe von Java Enhancement Proposals (JEP), darunter JEP 261, 200, 201, 220, 260 und 282. Diese legen fest, wie das Modulsystem konzeptionell aussieht, wie das JDK modularisiert ist und wie interne APIs des JDKs gekapselt werden.

Die wichtigsten Ziele von Jigsaw sind:

  • Reliable Configuration: Der fehleranfällige Classpath soll abgelöst werden. Wenn eine Klasse mehrfach (z. B. in mehreren JARs) auf ihm enthalten ist, entscheidet allein die Suchreihenfolge, welche Instanz der Klasse benutzt wird. Dies soll durch explizite Modul-Abhängigkeiten und den neuen Module-Path aufgelöst werden.
  • Strong Encapsulation: Ein Jigsaw-Module definiert sein öffentliches Komponenten-API. Andere Module können auf nicht-öffentliche Klassen nicht zugreifen, diese sind also gekapselt.
  • Scalable Platform: Die Java-Plattform ist selbst modularisiert. Dies ermöglicht, angepasste, schlankere Runtime-Images für eine Anwendung zu bauen und nur die Teile auszurollen, die wirklich benötigt werden.

Im Folgenden benutzen wir die Begriffe "Software-Komponente" sowie "Modul(e)" strukturell gleichwertig. Mit "Komponente" referenzieren wir den abstrakten Begriff eines Software-Bausteins in Software-Architektur und -Entwurf. Mit Jigsaw-"Module" bezeichnen wir eine konkrete Umsetzung im neuen Java-Module-System.

Modularity Patterns

Kirk Knoernschild veröffentlichte 2012 sein Buch "Java Application Architecture: Modularity Patterns with Examples Using OSGi" [3]. Darin sind knapp zwanzig Modularity Patterns beschrieben, also typische Design-Patterns, die ein gutes Design im Zusammenspiel von Komponenten festlegen. Der Pattern-Katalog ist auch online einsehbar [4]

Knoernschild zeigt die beispielhafte Umsetzung der Patterns mit OSGi, doch die Patterns gelten als allgemeingültige Prinzipien auch für Jigsaw: Mark Reinhold, Chef-Architekt für Java bei Oracle, bezieht sich in seinem Blogeintrag zu Jigsaw explizit auch auf dieses Buch und nennt die Patterns eine Grundlage für das Jigsaw-Design [5]

Wir greifen in diesem Artikel einige dieser Patterns auf und zeigen, ob und wie man sie mit Jigsaw umsetzen kann. Alle Grundlagen von Jigsaw haben wir in einem eigenen Tutorial beschrieben, für tiefergehende Details verweisen wir darauf [6]

Aus dem Pattern-Katalog greifen wir folgende Patterns auf. Wir beschreiben jeweils dessen Konzept sowie Vor- und Nachteile und erklären, ob und wie sich das Pattern mit Jigsaw-Mitteln umsetzen lässt:

  • Mit dem Pattern Manage Relationships werden die Grundsätze von Komponentenstrukturen und ihrer Beziehungen (Abhängigkeiten) beschrieben.
  • Das Pattern Acyclic Relationships verbietet zyklische Abhängigkeiten von Komponenten.
  • Mit dem Pattern Published Interface wird die Außensicht (Schnittstelle) einer Komponente von der Innensicht (Implementierungsgeheimnis) getrennt.
  • Die Patterns Module Facade und Separate Abstractions zeigen das Zusammenspiel von Komponenten.
  • Das Pattern Test Module besagt, dass eigene Test-Komponenten im Softwaretest benutzt werden sollen.
  • Das Pattern External Configuration sieht eine Trennung von Komponenten-Code und dessen Konfiguration vor.
  • Die Patterns Levelize Modules, Physical Layers und Levelized Build beschreiben, wie Komponenten zu "höherwertigen" Strukturen angeordnet werden können, also zu technischen Schichten und fachlichen Säulen.

Diese Modularity Patterns erinnern zu weiten Teilen an bekannte Design Patterns der Gang of Four [7], heben aber das dort beschriebene feingranulare Zusammenspiel auf OO-Klassenebene auf die grobgranularere Ebene von Komponenten. 

Modularity Pattern "Manage Relationships"

Mit Manage Relationships werden die Grundsätze von Komponentenstrukturen und ihrer Beziehungen (Abhängigkeiten) festgeschrieben. Damit werden also elementare Eigenschaften von Komponenten und ihren Beziehungen festgelegt. Der Name des Patterns greift eigentlich zu kurz, da er nur auf die Komponentenbeziehungen abhebt.

Jede Anwendung besteht aus einer oder mehreren Komponenten. Deren Abhängigkeiten werden explizit über die Schnittstellen modelliert. 

Eine Komponente ist, wie schon in der Einleitung beschrieben, ein eigenständiger, wiederverwendbarer Software-Baustein. Jede solche Komponente hat wohldefinierte Schnittstellen und definiert darüber Beziehungen und Abhängigkeiten. Dabei unterscheidet man

  • ausgehende Schnittstellen, also diejenigen Schnittstellen, über die eine Komponente von anderen abhängt, und
  • eingehende Schnittstellen, also diejenigen Schnittstellen, über die andere Komponenten auf eine Komponente zugreifen.

Umsetzung mit Jigsaw

Das Pattern lässt sich mit Jigsaw sehr gut umsetzen: Ein Jigsaw-Module gruppiert eine Reihe von Java-Packages plus ihrer Ressourcen. Ein solches Module wird kompiliert, als JAR-File paketiert und als sogenanntes "Modular JAR" auf den neuen Module-Path gelegt.

Im Module-Deskriptor module-info.java definiert man den Module-Namen, alle Module-Abhängigkeiten und legt außerdem Sichtbarkeitsregeln auf die Inhalte des Modules fest. Abb. 1 zeigt dies beispielhaft für zwei Module moda und modb.

Damit moda überhaupt Klassen aus modb benutzen kann, benötigt es eine sogenannte "Reads"-Abhängigkeit auf modb (blauer Pfeil in der Abbildung). Module-Abhängigkeiten werden also über deren Namen statisch modelliert. Die im Pattern beschriebenen Konzepte von Komponenten und ihrer Abhängigkeiten kann man als Jigsaw-Module umsetzen, ihre Abhängigkeiten über Reads-Beziehungen modellieren. Listing 1 zeigt den Module-Deskriptor von Module moda, in der auch die Abhängigkeit zu modb modelliert ist.

Listing 1: module-info.java von moda

module moda
{
    // statische Abhängigkeit auf modb
    requires modb;
}

Sowohl zur Compile- als auch zur Laufzeit müssen die Beziehungen aufgelöst werden können. Ist modb nicht verfügbar, so führt dies zu Compile- bzw. Laufzeit-Fehlern bei moda.

Einschränkungen

Jigsaw bringt dabei einige strukturelle Einschränkungen mit:

  • Jigsaw-Module können nicht versioniert werden. Ein Versionsflag ist zwar vorhanden, ist aber rein informell und spielt für das Laufzeitsystem keine Rolle.
  • Module können nicht gruppiert werden, insbesondere sind also auch keine Hierarchien möglich.
  • Beziehungen zwischen Modulen gelten nur lokal im gleichen "Prozessraum". Remote-Beziehungen zwischen Modulen sind nicht möglich.
  • Module haben – anders als bei OSGi – keinen Lebenszyklus. Man kann Module nicht dynamisch zur Laufzeit starten oder stoppen.
  • Beziehungen in Jigsaw können nicht mit einem Scope wie "Test", "Compile" oder "Runtime" versehen werden (wie sie beispielsweise in Maven verfügbar sind).

Modularity Pattern "Acyclic Relationships"

Das Pattern Acyclic Relationships verbietet zyklische Abhängigkeiten von Komponenten.

Vorteile

Die Komponenten eines Zyklus bilden de facto eine einzige große Komponente: Änderungen an einer ihrer Komponenten haben potentiell Auswirkungen auf alle anderen. Die Komponenten im Zyklus sind also stark gekoppelt – dies widerspricht dem Designprinzip der "Losen Kopplung" [8]

Dabei muss man zwischen Zyklen zur Compile-Zeit und Zyklen zur Laufzeit unterscheiden: Zyklische Abhängigkeiten zur Compile-Zeit beziehen sich auf die oben beschriebenen statischen Abhängigkeiten. Dagegen sind Zyklen zur Laufzeit weniger problematisch. Sie können entstehen, wenn sich mehrere Komponenten durch Aufrufe per Reflection gegenseitig und zyklisch referenzieren. Reflektive Aufrufe sind generisch und verursachen keine enge Kopplung.

Umsetzung mit Jigsaw

Auch dieses Pattern wird von Jigsaw umgesetzt, denn Jigsaw verbietet zyklische statische Abhängigkeiten per requires: Sie verursachen einen Compilefehler (Hinweis: In Java 9.0.4 führt ein Bug im Java-Compiler dazu, dass nicht alle Zyklen gefunden werden [9]). Zyklen zur Laufzeit durch reflektive Aufrufe sind bei Jigsaw erlaubt und verursachen keine Laufzeitfehler.

Dass Jigsaw keine Zyklen zulässt, kann man auch sehr schön sehen, wenn man wie in Abb. 2 eine Visualisierung der Abhängigkeiten der JDK-Module (nur java.*) vornimmt (erstellt mit dem Tool DepVis [10]).

Ganz rechts in diesem Modulegraphen ist das JDK-Module java.base zu sehen. Dieses Module enthält Basisklassen aus den Packages java.lang, java.io, java.util, java.net etc., die jedes andere Module immer benötigt. java.base hat keine ausgehenden, sondern nur eingehende Abhängigkeiten. Spätestens hier ist ein Zyklus beendet.

Modularity Pattern "Published Interface"

Mit dem Pattern Published Interface wird die Außensicht (Schnittstelle) einer Komponente von der Innensicht (Implementierungsgeheimnis) getrennt. Komponenten definieren ihre öffentliche Schnittstelle, deklarieren also explizit die Teile, die für andere Komponenten sichtbar und zugreifbar sein soll. Andere Komponenten können nur auf diese öffentliche Schnittstelle zugreifen. Somit lässt sich die "Schnittstelle" als Komponenten-Außensicht von dem "Implementierungsteil" als Komponenten-Innensicht klar trennen. Implementierungsgeheimnisse einer Komponente sind von außen nicht einsehbar und nicht aufrufbar. 

Vor- und Nachteile

Eine klare Trennung von Schnittstelle von Implementierungsgeheimnis ermöglicht, dass die Implementierung der Komponente geändert werden kann, ohne dass dies direkte Auswirkungen auf ihre Benutzer hat. Die Implementierung ist also gekapselt. Doch Vorsicht: Eine Schnittstelle stellt letztlich auch nur eine Abstraktion dar, und alle (nicht-trivialen) Abstraktionen sind i.d.R. unvollständig. Das besagt das "Law of Leaky Abstractions" von Joel Spolsky [11]. Eine Schnittstelle kann bei Änderungen nach außen syntaktisch gleichbleiben, also dieselben Operationen und Signaturen haben. Doch Eigenschaften der Implementierung lassen sich nicht ganz wegkapseln, z. B. wenn sich die Semantik in der Implementierung (z. B. durch andere Rückgaben in Sonderfällen wie "Rückgabe von null oder leerer Liste") – ändert; oder wenn sie andere nicht-funktionale Eigenschaften z. B. bezüglich Performance oder Latenz aufweist.

Schnittstellen sind nicht zuletzt eine gute Nutzungsdokumentation einer Komponente: Sie legen als API dessen Außensicht fest und umfassen all die Operationen, die eine Komponente nach außen anbietet.

Gute Komponenten-Schnittstellen zu entwerfen erfordert Sorgfalt und Erfahrung. Denn eine Schnittstelle sollte möglichst langfristig stabil sein: Sich ständig ändernde Schnittstellen haben natürlich unerwünschte Auswirkungen auf ihre Nutzer. Häufig muss das Design von Schnittstellen auch abwärtskompatibel sein. Das kann schnell komplex werden, wenn sich dies auf die Implementierung auswirkt, in der man z. B. mehrere Zweige und Versionen parallel pflegen muss.

Was ist Teil einer Java-Schnittstelle?

Was gehört alles zur öffentlichen Schnittstelle einer Java-Komponente? Sie beinhaltet alles, was ein Nutzer benötigt, um die Komponenten auch benutzen zu können:

  • Schnittstellen-Operationen (also alle Java-Interfaces) mit allen Operationen (Methoden),
  • Factories zum Erzeugen der Implementierung,
  • Datentypen, die als Eingabe-Parameter oder Rückgabe-Werte definiert sind, und
  • etwaige Exception-Typen.

Alle diese Teile zeigt das Interface in Listing 2:

Listing 2: Alle Teile einer Java-Schnittstelle

package pkg.myapi;

public interface MyInterface {
    public MyData myMethod (MyValue param) throws MyException;
}

Umsetzung mit Jigsaw

Betrachten wir nun Abb. 3: Das Module modb definiert über seine "Exports" (roter Pfeil), welche seiner Packages für andere Module überhaupt sichtbar und damit zugreifbar sind. Die Granularität eines solchen Exports ist dabei immer ein Java-Package. In unserem Beispiel darf moda nur auf das Package pkg.myapi von modb zugreifen, nicht aber auf dessen internes Package pkg.myimpl.

Per Default sind in einem Jigsaw-Module keine Packages exportiert: Was ein Module also nicht ausdrücklich mit Exports freigibt, ist für andere Module nicht sichtbar. Alle Regeln zum Zugriffsschutz greifen übrigens auch im JDK selber. Mit der Einführung von Jigsaw wurde das JDK komplett modularisiert, so dass nun verschiedene JDK-Packages nicht mehr zugreifbar sind.

In einem Jigsaw-Module sind nur exportierte Packages von außen zugreifbar. Da der Export nur auf Package-Ebene möglich ist, muss man zur Umsetzung des Modularity Patterns "Published Interface" Schnittstelle und Implementierung in verschiedene Packages legen. Die Abb. 3 und 4 zeigen den Export des Packages pkg.myapi von modb, während die Implementierung in pkg.myimpl gekapselt ist (da nicht exportiert).

Sowohl zur Compile- als auch zur Laufzeit werden bei einem Zugriff von außerhalb des Modules Zugriffsprüfungen in dieser Reihenfolge vorgenommen:

  1. Prüfung der Readability (read).
  2. Prüfung der Accessibility (exports).
  3. Prüfung der Sichtbarkeitsmodifier public, private, protected, package-sichtbar.

Beim Zugriff über Modulegrenzen hinweg muss also zunächst eine statische Reads-Abhängigkeit per requires definiert sein. Danach wird geprüft, ob ein Package per exports auch exportiert ist. Schlussendlich greifen danach als drittes die Checks auf die altbekannten Sichtbarkeitsmodifier. Innerhalb eines Modules ändert sich bei Sichtbarkeitsprüfungen nichts gegenüber Java 8 und davor – innerhalb von modb sehen sich alle Klassen und nur die dritte Prüfung greift. 

Typensichtbarkeit

Der Zugriffsschutz in Jigsaw regelt letztlich die Typensichtbarkeit, welche Java-Typen welcher Packages für andere Module sichtbar und nutzbar sind. 

Dabei behalten Objektinstanzen zur Laufzeit auch über Modulegrenzen hinweg natürlich immer ihren Typ. Abb. 5 und Listing 3 zeigen ein Beispiel: Die Klasse MyImpl im nicht-exportierten Package pkg.myimpl implementiert das Interface MyInterface im exportierten Package pkg.myapi. MyFactory gibt eine Instanz der Klasse MyImpl zurück, aber als MyInterface

Der Caller aus moda kann sich von MyFactory eine MyImpl-Instanz zurückgeben lassen, kann dann aber nur die in MyInterface aufgeführten Methoden aufrufen. Insbesondere ist in moda kein Cast auf Typ MyImpl möglich, da dieser Typ dort gar nicht sichtbar ist.

Listing 3: Erzeugen einer Instanz von MyInterface über MyFactory

public class MyFactory {

    public MyInterface create() {     
        return new MyImpl();
    }
…
public class Caller {
…
    MyInterface myInt = new MyFactory().create();
…

Dasselbe Prinzip der Typensichtbarkeit gilt auch bei Vererbung und auch bei Exception-Typen. Ist ein Typ einer Klasse nicht sichtbar, so muss beim Aufrufer immer ein allgemeinerer, abstrakterer Typ benutzt werden, der dort auch sichtbar ist. Im Falle von Exception-Catch muss der Aufrufer also zum Beispiel die Oberklassen java.lang.Exception bzw. java.lang.RuntimeException fangen.

Gerichtete Exports und Reflection

Schaut man sich die Module-Deskriptoren von JDK-Modulen an, trifft man öfter auf gerichtete Exports der Form exports <package> to <module>. Mit diesem Mechanismus kann man ein Package gezielt nur für ein oder mehrere bestimmte andere Ziel-Module exportieren und so eine engere Zusammengehörigkeit in einer Art Komponentengruppe ausdrücken. Das JDK nutzt diesen Mechanismus ausgiebig, weil einige Altlasten (z. B. alter Sun-Packages) noch nicht abgelöst sind, diese aber nur innerhalb des JDK sichtbar sein sollen.

Auch Zugriffe mittels Reflection (bzw. Reflective Access) sind geschützt [5]. Ein Zugriff für solche reinen Laufzeit-Zugriffe kann im Module-Deskriptor mit opens (bzw. auch gerichtet mit opens to) ermöglicht werden.

Lesen Sie hier Teil 2

Modularity Patterns mit dem Java-Modulsystem Jigsaw [Teil 2]

Mit Jigsaw eine bessere, modulare Software-Architektur erstellen: Wir betrachten bekannte Modularity Design Patterns und zeigen, ob und wie man diese mit Jigsaw umsetzen kann.
>> Weiterlesen
Quellen
  1. C. Szyperski: Component Software: Beyond Object-Oriented Programming. Addison Wesley
  2. UML Specification
  3. K. Knoernschild: Java Application Architecture: Modularity Patterns With Examples Using Osgi
  4. Modularity-Pattern-Katalog
  5. M. Reinhold: There’s not a moment to lose!
  6. Informatik Aktuell – Dr. R. Grammes, M. Lehmann, Dr. K. Schaal: Java 9 bringt das neue Modulsystem Jigsaw
  7. E. Gamma, R. Helm, R. E. Johnson, J. Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software. Pearson Professional
  8. Eberhard Wolff: Zyklische Abhängigkeiten – eine Architektur-Todsünde?
  9. Java-Compiler-Bug zu Zyklen
  10. DepVis-Tool auf Github zur Visualisierung von Jigsaw-Modulgraphen
  11. J. Spolsky: The Law of Leaky Abstractions 

Autoren

Dr. Kristine Schaal

Dr. Kristine Schaal arbeitet seit fast 20 Jahren in der Softwareentwicklung und ist in Projekten der Individualentwicklung für Kunden verschiedener Branchen unterwegs, technisch überwiegend im Java-Umfeld.
>> Weiterlesen

Martin Lehmann

Seit Ende der 90er Jahre wirkt Martin Lehmann als Softwareentwickler und -architekt in der Softwareentwicklung in diversen Projekten der Individualentwicklung für Kunden verschiedener Branchen.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben