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; // можемо використовувати утиліти напряму
}
Правила та найкращі практики
- Уникайте циклічних залежностей. Якщо A requires B і B requires A — це дефект дизайну. Зазвичай вирішується винесенням спільного common/api.
- Мінімізуйте залежності. Не підключайте модуль, якщо він насправді не потрібен.
- Експортуйте використовувані пакети. Класи мають перебувати в пакетах, оголошених через exports, інакше буде помилка компіляції.
- Утиліти — максимально незалежні. utils не має залежати від бізнес-логіки.
4. Практика: приклад поділу навчального проєкту на три модулі
Структура тек
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 ← батьківський
├─ 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».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ