1. Wann und warum ein Projekt in Module aufteilen
Warum Sie nicht alles in ein einziges Modul packen sollten
Wenn Sie eine kleine Laborübung oder "Hello, World!" schreiben, kann das Modulsystem überdimensioniert wirken. Doch mit dem Wachstum des Projekts – Dutzende und Hunderte Klassen, zahlreiche Pakete, externe Bibliotheken – wird Chaos unvermeidlich. Das ist wie eine Bibliothek ohne Regale: Solange es nur wenige Bücher gibt, ist es erträglich, später wird das Finden jedoch schwierig. Module sind Ihre Regale: Sie helfen, Ordnung zu schaffen, und verbergen die „Küche“ (die Implementierung), während nach außen nur die „Vitrine“ (das API) sichtbar bleibt.
Warum in Module aufteilen
- Trennung der Verantwortlichkeiten: Jedes Modul ist für seinen Bereich zuständig (z. B. Datenbank, Geschäftslogik, UI).
- Wiederverwendung von Code: Ein Modul kann in einem anderen Projekt eingebunden werden.
- Bessere Testbarkeit: Module werden unabhängig getestet.
- Sicherheit und Kapselung: Nach außen ist nur das API sichtbar, die Implementierung bleibt verborgen.
- Erleichterte Wartung: weniger „magische“ Verknüpfungen, eine verständliche Abhängigkeitskarte.
- Schneller Build und Deployment: es werden nur geänderte Module neu gebaut.
Wann in Module aufteilen
- Das Projekt wird zu groß für eine einzelne Entwicklerperson (oder die IDE beginnt zu „ruckeln“).
- Teile kristallisieren sich klar heraus: core, ui, utils, api, impl.
- Es ist eine Wiederverwendung von Code in anderen Projekten geplant.
- Es gibt externe Abhängigkeiten, die nur in Teilen des Projekts benötigt werden.
- Implementierungsdetails (Algorithmen, interne Klassen) sollen verborgen werden.
2. Typische Modularisierungsschemata
Im Folgenden – beliebte Aufteilungsschemata, geeignet für Lern- und Produktivprojekte.
„Zwiebel“-Architektur (Onion Architecture)
Die äußere Schicht hängt von der inneren ab, aber nicht umgekehrt.
[ app (UI) ]
↓
[ core (Logik) ]
↓
[ utils (Hilfsfunktionen) ]
- app – äußeres Modul: grafische Oberfläche, Web-Anwendung, Einstiegspunkt (main).
- core – Geschäftslogik, Modelle, Services.
- utils – Hilfsklassen.
Regel: Die innere Schicht darf nicht von der äußeren abhängen. So kann core in verschiedenen Oberflächen wiederverwendet werden (Konsole, Web, Desktop).
Module für API und Implementierung
Für Bibliotheken ist es praktisch, Schnittstellen und ihre Implementierung zu trennen:
[ mylib.api ] ← exportiert nur Schnittstellen
[ mylib.impl ] ← enthält Implementierungen, wird nicht exportiert
Module für Tests
Tests werden häufig in ein separates Modul ausgelagert, damit sie nicht in das Produktionsartefakt gelangen.
[ app ]
[ core ]
[ core.tests ]
Schema für ein Lernprojekt
myeditor/
├─ app/ ← Einstiegspunkt, Start der Anwendung
├─ core/ ← Geschäftslogik (Arbeit mit Dateien, Text)
└─ utils/ ← Hilfsfunktionen (Logging, Parsing)
3. Abhängigkeiten zwischen Modulen
In Java-Modulen werden Abhängigkeiten explizit in module-info.java mit dem Schlüsselwort requires angegeben. Das erhöht die Lesbarkeit und ermöglicht es dem Compiler/der JVM, die Verfügbarkeit des API über exports zu kontrollieren.
Beispiel einer Abhängigkeit
core/module-info.java
module myeditor.core {
exports myeditor.core.api; // nach außen ist nur das Paket api sichtbar
requires myeditor.utils; // wir verwenden die Utils
}
app/module-info.java
module myeditor.app {
requires myeditor.core; // wir verwenden core
requires myeditor.utils; // wir können die Utils direkt verwenden
}
Regeln und Best Practices
- Vermeiden Sie zyklische Abhängigkeiten. Wenn A requires B und B requires A – das ist ein Designfehler. Meist wird das durch Auslagern eines gemeinsamen common/api gelöst.
- Minimieren Sie Abhängigkeiten. Binden Sie kein Modul ein, wenn es nicht wirklich benötigt wird.
- Exportieren Sie die verwendeten Pakete. Klassen müssen in Paketen liegen, die über exports deklariert sind, sonst gibt es einen Kompilierfehler.
- Utils – so unabhängig wie möglich. utils sollte nicht von der Geschäftslogik abhängen.
4. Praxis: Beispielaufteilung eines Lernprojekts in 3 Module
Verzeichnisstruktur
myeditor/
├─ app/
│ ├─ src/
│ │ └─ myeditor/app/Main.java
│ └─ module-info.java
├─ core/
│ ├─ src/
│ │ ├─ myeditor/core/api/TextService.java
│ │ └─ myeditor/core/impl/TextServiceImpl.java
│ └─ module-info.java
└─ utils/
├─ src/
│ └─ myeditor/utils/Logger.java
└─ module-info.java
Beispiele für module-info.java
core/module-info.java
module myeditor.core {
exports myeditor.core.api;
requires myeditor.utils;
}
app/module-info.java
module myeditor.app {
requires myeditor.core;
requires myeditor.utils;
}
utils/module-info.java
module myeditor.utils {
exports myeditor.utils;
}
Codebeispiel (TextService)
myeditor/core/api/TextService.java
package myeditor.core.api;
public interface TextService {
String toUpperCase(String text);
}
myeditor/core/impl/TextServiceImpl.java
package myeditor.core.impl;
import myeditor.core.api.TextService;
public class TextServiceImpl implements TextService {
@Override
public String toUpperCase(String text) {
return text.toUpperCase();
}
}
myeditor/app/Main.java
package myeditor.app;
import myeditor.core.api.TextService;
import myeditor.core.impl.TextServiceImpl;
public class Main {
public static void main(String[] args) {
TextService service = new TextServiceImpl();
System.out.println(service.toUpperCase("hello, modules!"));
}
}
So sieht es in IntelliJ IDEA aus
- Jedes Verzeichnis ist ein eigenes Modul in der Projektstruktur.
- Jedes Modul hat seine eigene module-info.java im Stamm von src.
- Beim Start von main aus app wählt die IDE den module-path automatisch.
- Der Versuch, eine Klasse aus einem nicht exportierten Paket zu verwenden, endet mit einem Kompilierfehler.
5. Auswirkungen auf den Build: Maven/Gradle und Module
Maven
Ein Mehrmodulprojekt besteht aus einem „Eltern“-Projekt (parent) und mehreren „Kind“-Modulen.
myeditor/
├─ pom.xml ← parent
├─ app/
│ └─ pom.xml
├─ core/
│ └─ pom.xml
└─ utils/
└─ pom.xml
Beispiel: parent pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>myeditor</groupId>
<artifactId>myeditor-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>app</module>
<module>core</module>
<module>utils</module>
</modules>
</project>
Besonderheiten:
- Maven berücksichtigt module-info.java beim Kompilieren.
- Zum Ausführen wird --module-path statt --classpath verwendet.
- Wenn exports oder requires vergessen wurden – gibt es einen Kompilierfehler.
Gradle
Mehrmodularität wird über settings.gradle und separate build.gradle je Modul konfiguriert.
settings.gradle
rootProject.name = 'myeditor'
include 'app', 'core', 'utils'
build.gradle für ein Modul
plugins {
id 'java'
}
java {
modularity.inferModulePath = true
}
IntelliJ IDEA
- IDEA kann beim Erstellen eines Java-Moduls eine module-info.java erzeugen.
- Mit Maven/Gradle wird die Modulstruktur automatisch erkannt.
- Beim Start von main aus app konfiguriert die IDE den module-path.
- Import-/Export-Dialoge zeigen die Sichtbarkeit von Paketen und Modulen an.
Typische Fehler bei der Aufteilung in Module
Fehler Nr. 1: Zyklische Abhängigkeiten zwischen Modulen. Wenn zwei Module requires gegenseitig aufeinander deklarieren, meldet der Compiler einen Fehler. Das ist häufig ein Zeichen für eine aus dem Ruder gelaufene Architektur. Lösung – ein gemeinsames api-Modul herausziehen oder die Grenzen neu zuschneiden.
Fehler Nr. 2: Verwendung von Klassen aus nicht exportierten Paketen. Eine Klasse kann zwar public sein, aber wenn das Paket nicht in exports in module-info.java angegeben ist, sieht ein anderes Modul sie nicht. Ergebnis – Kompilierfehler.
Fehler Nr. 3: requires für ein verwendetes Modul vergessen. Ein Import aus einem anderen Modul ohne entsprechenden Eintrag in module-info.java lässt sich nicht kompilieren. Deklarieren Sie Abhängigkeiten stets explizit.
Fehler Nr. 4: Doppelte Modulnamen. Modulenamen müssen innerhalb des Builds eindeutig sein (insbesondere mit Maven/Gradle). Duplikate zerstören den Build.
Fehler Nr. 5: Falsche Verzeichnisstruktur. Die Datei module-info.java muss im Stamm von src des jeweiligen Moduls liegen. Andernfalls findet der Compiler das Modul nicht.
Fehler Nr. 6: Falscher module-path beim Starten. Beim manuellen Start verwenden Sie --module-path statt --classpath, sonst erhalten Sie „module not found“.
GO TO FULL VERSION