JavaRush /Курси /JAVA 25 SELF /Розділення проєкту на модулі: найкращі практики

Розділення проєкту на модулі: найкращі практики

JAVA 25 SELF
Рівень 60 , Лекція 3
Відкрита

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».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ