1. Cuándo y por qué dividir un proyecto en módulos
Por qué no conviene hacerlo todo en un único módulo
Si escribes una pequeña práctica de laboratorio o "Hello, World!", el sistema modular puede parecer excesivo. Pero a medida que el proyecto crece —decenas y cientos de clases, multitud de paquetes, bibliotecas de terceros— el caos se vuelve inevitable. Es como una biblioteca sin estanterías: mientras hay pocos libros, es pasable, pero luego es difícil encontrar algo. Los módulos son tus estanterías: ayudan a poner orden y ocultan la «cocina» (la implementación), dejando fuera solo el «escaparate» (API).
Para qué dividir en módulos
- Separación de responsabilidades: cada módulo se hace cargo de su ámbito (por ejemplo, BD, lógica de negocio, UI).
- Reutilización de código: un módulo puede conectarse en otro proyecto.
- Mejora de la capacidad de prueba: los módulos se prueban de forma independiente.
- Seguridad e encapsulación: hacia fuera solo se ve el API; la implementación queda oculta.
- Facilita el mantenimiento: menos dependencias «mágicas», un mapa de dependencias claro.
- Compilación y despliegue más rápidos: solo se reconstruyen los módulos que han cambiado.
Cuándo dividir en módulos
- El proyecto se vuelve demasiado grande para un solo desarrollador (o la IDE empieza a ir lenta).
- Se distinguen claramente partes: core, ui, utils, api, impl.
- Se planea reutilizar código en otros proyectos.
- Hay dependencias externas que solo necesita una parte del proyecto.
- Es necesario ocultar detalles de implementación (algoritmos, clases internas).
2. Esquemas típicos de modularidad
A continuación encontrarás esquemas de división populares, adecuados tanto para proyectos de aprendizaje como para proyectos reales.
Arquitectura «Onion» (Onion Architecture)
La capa externa depende de la interna, pero no al revés.
[ app (UI) ]
↓
[ core (lógica) ]
↓
[ utils (utilidades) ]
- app — módulo externo: interfaz gráfica, aplicación web, punto de entrada (main).
- core — lógica de negocio, modelos, servicios.
- utils — clases auxiliares.
Regla: la capa interna no debe depender de la externa. Así core se puede reutilizar en distintas interfaces (consola, web, escritorio).
Módulos para API e implementación
Para bibliotecas conviene separar las interfaces de su implementación:
[ mylib.api ] ← exporta solo interfaces
[ mylib.impl ] ← contiene implementaciones, no se exporta
Módulos para tests
A menudo se separan los tests en un módulo aparte para que no terminen en el artefacto de producción.
[ app ]
[ core ]
[ core.tests ]
Esquema para un proyecto didáctico
myeditor/
├─ app/ ← punto de entrada, arranque de la aplicación
├─ core/ ← lógica de negocio (trabajo con archivos y texto)
└─ utils/ ← utilidades (logging, análisis/parsing)
3. Dependencias entre módulos
En los módulos de Java, las dependencias se especifican explícitamente en module-info.java mediante la palabra clave requires. Esto mejora la legibilidad y permite al compilador/JVM controlar la accesibilidad del API mediante exports.
Ejemplo de dependencia
core/module-info.java
module myeditor.core {
exports myeditor.core.api; // solo es visible hacia fuera el paquete api
requires myeditor.utils; // usamos utilidades
}
app/module-info.java
module myeditor.app {
requires myeditor.core; // usamos core
requires myeditor.utils; // podemos usar utilidades directamente
}
Reglas y buenas prácticas
- Evita las dependencias cíclicas. Si A requires B y B requires A, es un defecto de diseño. Normalmente se resuelve extrayendo un common/api común.
- Minimiza las dependencias. No añadas un módulo si realmente no hace falta.
- Exporta los paquetes utilizados. Las clases deben estar en paquetes declarados con exports; de lo contrario, habrá error de compilación.
- Utilidades: lo más independientes posible. utils no debe depender de la lógica de negocio.
4. Práctica: ejemplo de división de un proyecto didáctico en 3 módulos
Estructura de carpetas
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
Ejemplos de 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;
}
Ejemplo de código (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!"));
}
}
Cómo se ve en IntelliJ IDEA
- Cada directorio es un módulo independiente en la estructura del proyecto.
- Cada módulo tiene su propio module-info.java en la raíz de src.
- Al ejecutar main desde app, la IDE ajustará por sí sola el module-path.
- Intentar usar una clase de un paquete no exportado terminará con un error de compilación.
5. Impacto en la compilación: Maven/Gradle y módulos
Maven
Un proyecto multimódulo consta de un proyecto «padre» (parent) y varios módulos «hijo».
myeditor/
├─ pom.xml ← parent
├─ app/
│ └─ pom.xml
├─ core/
│ └─ pom.xml
└─ utils/
└─ pom.xml
Ejemplo de 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>
Particularidades:
- Maven tiene en cuenta module-info.java durante la compilación.
- Para ejecutar se usa --module-path en lugar de --classpath.
- Si olvidas exports o requires, obtendrás un error de compilación.
Gradle
La multimodularidad se configura mediante settings.gradle y un build.gradle por módulo.
settings.gradle
rootProject.name = 'myeditor'
include 'app', 'core', 'utils'
build.gradle del módulo
plugins {
id 'java'
}
java {
modularity.inferModulePath = true
}
IntelliJ IDEA
- IDEA puede crear module-info.java al crear un módulo de Java.
- Con Maven/Gradle la estructura de módulos se detecta automáticamente.
- Al ejecutar main desde app, la IDE configurará module-path.
- Los cuadros de diálogo de import/export ayudan con la visibilidad de paquetes y módulos.
Errores típicos al dividir en módulos
Error n.º 1: Dependencias cíclicas entre módulos. Si dos módulos declaran requires uno del otro, el compilador dará error. Normalmente es señal de una arquitectura degradada. Solución: extraer un módulo api común o replantear los límites.
Error n.º 2: Uso de clases de paquetes no exportados. Una clase puede ser public, pero si el paquete no está indicado en exports en module-info.java, otro módulo no la verá. Resultado: error de compilación.
Error n.º 3: Olvidar añadir requires para el módulo utilizado. Importar desde otro módulo sin la entrada correspondiente en module-info.java no compilará. Declara siempre las dependencias de forma explícita.
Error n.º 4: Duplicación de nombres de módulos. Los nombres de los módulos deben ser únicos dentro de la compilación (especialmente con Maven/Gradle). Los duplicados rompen la compilación.
Error n.º 5: Estructura de directorios incorrecta. El archivo module-info.java debe estar en la raíz de src del módulo correspondiente. De lo contrario, el compilador no encontrará el módulo.
Error n.º 6: module-path incorrecto al ejecutar. Al ejecutar manualmente, especifica --module-path en lugar de --classpath; si no, verás «module not found».
GO TO FULL VERSION