1. Когда и зачем делить проект на модули
Почему не стоит делать всё в одном модуле
Если вы пишете маленькую лабораторную или "Hello, World!", то модульная система может показаться избыточной. Но по мере роста проекта — десятки и сотни классов, множество пакетов, сторонние библиотеки — хаос становится неизбежным. Это как библиотека без полок: пока книг мало — сносно, но дальше найти что-то трудно. Модули — это ваши полки: они помогают навести порядок и скрывают «кухню» (реализацию), оставляя снаружи только «витрину» (API).
Зачем делить на модули
- Разделение ответственности: каждый модуль отвечает за свою область (например, БД, бизнес-логика, UI).
- Переиспользование кода: модуль можно подключить в другой проект.
- Улучшение тестируемости: модули тестируются независимо.
- Безопасность и инкапсуляция: наружу виден только API, реализация скрыта.
- Облегчение поддержки: меньше «магических» связей, понятная карта зависимостей.
- Быстрая сборка и деплой: пересобираются только изменившиеся модули.
Когда делить на модули
- Проект становится слишком большим для одного разработчика (или IDE начинает «тормозить»).
- Чётко выделяются части: core, ui, utils, api, impl.
- Планируется переиспользование кода в других проектах.
- Есть внешние зависимости, которые нужны только части проекта.
- Нужно скрыть детали реализации (алгоритмы, внутренние классы).
2. Типовые схемы модульности
Ниже — популярные схемы разбиения, подходящие для учебных и боевых проектов.
«Луковая» архитектура (Onion Architecture)
Внешний слой зависит от внутреннего, но не наоборот.
[ app (UI) ]
↓
[ core (логика) ]
↓
[ utils (утилиты) ]
- app — внешний модуль: графический интерфейс, веб-приложение, точка входа (main).
- core — бизнес-логика, модели, сервисы.
- utils — вспомогательные классы.
Правило: внутренний слой не должен зависеть от внешнего. Так core можно переиспользовать в разных интерфейсах (консоль, веб, десктоп).
Модули для API и реализации
Для библиотек удобно выделять интерфейсы и их реализацию отдельно:
[ mylib.api ] ← экспортирует только интерфейсы
[ mylib.impl ] ← содержит реализации, не экспортируется
Модули для тестов
Тесты часто выносят в отдельный модуль, чтобы не попадали в боевой артефакт.
[ app ]
[ core ]
[ core.tests ]
Схема для учебного проекта
myeditor/
├─ app/ ← точка входа, запуск приложения
├─ core/ ← бизнес-логика (работа с файлами, текстом)
└─ utils/ ← утилиты (логирование, парсинг)
3. Зависимости между модулями
В Java-модулях зависимости явно указываются в module-info.java с помощью ключевого слова requires. Это и повышает читаемость, и позволяет компилятору/JVM контролировать доступность API через exports.
Пример зависимости
core/module-info.java
module myeditor.core {
exports myeditor.core.api; // наружу виден только пакет api
requires myeditor.utils; // используем утилиты
}
app/module-info.java
module myeditor.app {
requires myeditor.core; // используем core
requires myeditor.utils; // можем использовать утилиты напрямую
}
Правила и best practices
- Избегайте циклических зависимостей. Если A requires B и B requires A — это дефект дизайна. Обычно решается выносом общего common/api.
- Минимизируйте зависимости. Не подключайте модуль, если он реально не нужен.
- Экспортируйте используемые пакеты. Классы должны находиться в пакетах, объявленных через exports, иначе будет ошибка компиляции.
- Утилиты — максимально независимы. utils не должен зависеть от бизнес-логики.
4. Практика: пример разбиения учебного проекта на 3 модуля
Структура папок
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
Примеры 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;
}
Пример кода (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!"));
}
}
Как это выглядит в IntelliJ IDEA
- Каждый каталог — отдельный Module в структуре проекта.
- У каждого модуля свой module-info.java в корне src.
- При запуске main из app IDE сама подберёт module-path.
- Попытка использовать класс из неэкспортируемого пакета завершится ошибкой компиляции.
5. Влияние на сборку: Maven/Gradle и модули
Maven
Многомодульный проект — это «родительский» проект (parent) и несколько «дочерних» модулей.
myeditor/
├─ pom.xml ← parent
├─ app/
│ └─ pom.xml
├─ core/
│ └─ pom.xml
└─ utils/
└─ pom.xml
Пример 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>
Особенности:
- Maven учитывает module-info.java при компиляции.
- Для запуска используется --module-path вместо --classpath.
- Если забыли exports или requires — получите ошибку компиляции.
Gradle
Многомодульность настраивается через settings.gradle и отдельные build.gradle для модулей.
settings.gradle
rootProject.name = 'myeditor'
include 'app', 'core', 'utils'
build.gradle для модуля
plugins {
id 'java'
}
java {
modularity.inferModulePath = true
}
IntelliJ IDEA
- IDEA умеет создавать module-info.java при создании Java-модуля.
- С Maven/Gradle структура модулей подхватывается автоматически.
- При запуске main из app IDE настроит module-path.
- Диалоги импорта/экспорта подсказывают видимость пакетов и модулей.
Типичные ошибки при разбиении на модули
Ошибка №1: Циклические зависимости между модулями. Если два модуля объявляют requires друг на друга, компилятор выдаст ошибку. Обычно это признак «поплывшей» архитектуры. Решение — выделить общий api-модуль или пересмотреть границы.
Ошибка №2: Использование классов из неэкспортируемых пакетов. Класс может быть public, но если пакет не указан в exports в module-info.java, другой модуль его не увидит. Итог — ошибка компиляции.
Ошибка №3: Забыли добавить requires для используемого модуля. Импорт из другого модуля без соответствующей записи в module-info.java не скомпилируется. Всегда объявляйте зависимости явно.
Ошибка №4: Дублирование имён модулей. Имена модулей должны быть уникальны в рамках сборки (особенно с Maven/Gradle). Дубликаты ломают сборку.
Ошибка №5: Неправильная структура каталогов. Файл module-info.java должен лежать в корне src соответствующего модуля. Иначе компилятор не найдёт модуль.
Ошибка №6: Неправильный module-path при запуске. При ручном запуске указывайте --module-path вместо --classpath, иначе получите «module not found».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ