1. Quando e perché suddividere il progetto in moduli
Perché non conviene fare tutto in un unico modulo
Se state scrivendo un piccolo laboratorio o "Hello, World!", il sistema modulare può sembrare eccessivo. Ma con la crescita del progetto — decine e centinaia di classi, molti package, librerie di terze parti — il caos diventa inevitabile. È come una biblioteca senza scaffali: finché i libri sono pochi — va bene, ma poi diventa difficile trovare qualcosa. I moduli sono i vostri scaffali: aiutano a mettere ordine e nascondono la «cucina» (l’implementazione), lasciando all’esterno solo la «vetrina» (l’API).
Perché suddividere in moduli
- Separazione delle responsabilità: ogni modulo è responsabile del proprio ambito (per esempio, DB, logica di business, UI).
- Riutilizzo del codice: un modulo può essere collegato a un altro progetto.
- Migliore testabilità: i moduli si testano in modo indipendente.
- Sicurezza e incapsulamento: verso l’esterno è visibile solo l’API, l’implementazione è nascosta.
- Semplificazione della manutenzione: meno collegamenti «magici», mappa delle dipendenze chiara.
- Build e deploy più rapidi: si ricompilano solo i moduli modificati.
Quando suddividere in moduli
- Il progetto diventa troppo grande per un solo sviluppatore (oppure l’IDE inizia a «rallentare»).
- Si delineano chiaramente le parti: core, ui, utils, api, impl.
- Si prevede di riutilizzare il codice in altri progetti.
- Ci sono dipendenze esterne necessarie solo a una parte del progetto.
- Occorre nascondere i dettagli di implementazione (algoritmi, classi interne).
2. Schemi tipici di modularità
Di seguito — schemi di suddivisione popolari, adatti sia a progetti didattici sia a quelli reali.
Architettura «a cipolla» (Onion Architecture)
Lo strato esterno dipende da quello interno, ma non viceversa.
[ app (UI) ]
↓
[ core (logica) ]
↓
[ utils (utility) ]
- app — modulo esterno: interfaccia grafica, applicazione web, punto d’ingresso (main).
- core — logica di business, modelli, servizi.
- utils — classi di supporto.
Regola: lo strato interno non deve dipendere da quello esterno. Così core può essere riutilizzato in interfacce diverse (console, web, desktop).
Moduli per API e implementazione
Per le librerie è comodo separare le interfacce dalla loro implementazione:
[ mylib.api ] ← esporta solo le interfacce
[ mylib.impl ] ← contiene le implementazioni, non viene esportato
Moduli per i test
Spesso i test vengono spostati in un modulo separato, per non finire nell’artefatto di produzione.
[ app ]
[ core ]
[ core.tests ]
Schema per un progetto didattico
myeditor/
├─ app/ ← punto d’ingresso, avvio dell’applicazione
├─ core/ ← logica di business (lavoro con file e testo)
└─ utils/ ← utility (logging, parsing)
3. Dipendenze tra moduli
Nei moduli Java le dipendenze sono indicate esplicitamente in module-info.java con la parola chiave requires. Questo aumenta la leggibilità e permette al compilatore/JVM di controllare l’accessibilità dell’API tramite exports.
Esempio di dipendenza
core/module-info.java
module myeditor.core {
exports myeditor.core.api; // è visibile all'esterno solo il pacchetto api
requires myeditor.utils; // usiamo le utility
}
app/module-info.java
module myeditor.app {
requires myeditor.core; // usiamo core
requires myeditor.utils; // possiamo usare le utility direttamente
}
Regole e best practice
- Evitate le dipendenze cicliche. Se A requires B e B requires A — è un difetto di design. Di solito si risolve estraendo un common/api comune.
- Minimizzate le dipendenze. Non aggiungete un modulo se non è realmente necessario.
- Esportate i pacchetti utilizzati. Le classi devono trovarsi nei pacchetti dichiarati tramite exports, altrimenti si avrà un errore di compilazione.
- Le utility — il più indipendenti possibile. utils non deve dipendere dalla logica di business.
4. Pratica: esempio di suddivisione di un progetto didattico in 3 moduli
Struttura delle cartelle
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
Esempi di 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;
}
Esempio di codice (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!"));
}
}
Come appare in IntelliJ IDEA
- Ogni cartella è un Module separato nella struttura del progetto.
- Ogni modulo ha il proprio module-info.java alla radice di src.
- Avviando main da app l’IDE imposterà automaticamente il module-path.
- Il tentativo di usare una classe da un pacchetto non esportato si concluderà con un errore di compilazione.
5. Impatto sulla build: Maven/Gradle e moduli
Maven
Un progetto multi-modulo è un progetto «genitore» (parent) e diversi moduli «figli».
myeditor/
├─ pom.xml ← parent
├─ app/
│ └─ pom.xml
├─ core/
│ └─ pom.xml
└─ utils/
└─ pom.xml
Esempio di 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>
Particolarità:
- Maven tiene conto di module-info.java in fase di compilazione.
- Per l’esecuzione si usa --module-path invece di --classpath.
- Se dimenticate exports o requires — otterrete un errore di compilazione.
Gradle
La multi-modularità si configura tramite settings.gradle e build.gradle separati per i moduli.
settings.gradle
rootProject.name = 'myeditor'
include 'app', 'core', 'utils'
build.gradle per un modulo
plugins {
id 'java'
}
java {
modularity.inferModulePath = true
}
IntelliJ IDEA
- IDEA può creare module-info.java durante la creazione di un modulo Java.
- Con Maven/Gradle, la struttura dei moduli viene rilevata automaticamente.
- Avviando main da app, l’IDE configurerà il module-path.
- Le finestre di dialogo di import/export suggeriscono la visibilità di pacchetti e moduli.
Errori tipici nella suddivisione in moduli
Errore n. 1: dipendenze cicliche tra moduli. Se due moduli dichiarano requires l’uno dell’altro, il compilatore darà un errore. Di solito è il segno di un’architettura deteriorata. Soluzione — estrarre un modulo api comune oppure rivedere i confini.
Errore n. 2: utilizzo di classi da pacchetti non esportati. Una classe può essere public, ma se il pacchetto non è indicato in exports in module-info.java, un altro modulo non la vedrà. Risultato — errore di compilazione.
Errore n. 3: dimenticato di aggiungere requires per il modulo usato. Un import da un altro modulo senza la voce corrispondente in module-info.java non verrà compilato. Dichiarate sempre esplicitamente le dipendenze.
Errore n. 4: duplicazione dei nomi dei moduli. I nomi dei moduli devono essere univoci all’interno della build (soprattutto con Maven/Gradle). I duplicati rompono la build.
Errore n. 5: struttura delle cartelle errata. Il file module-info.java deve trovarsi nella radice di src del relativo modulo. Altrimenti il compilatore non troverà il modulo.
Errore n. 6: module-path errato in fase di esecuzione. Nel lancio manuale indicate --module-path invece di --classpath, altrimenti otterrete «module not found».
GO TO FULL VERSION