JavaRush /Курсы /JAVA 25 SELF /Разделение проекта на модули: best practices

Разделение проекта на модули: best practices

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;   // можем использовать утилиты напрямую
}

Правила и 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».

1
Задача
JAVA 25 SELF, 60 уровень, 3 лекция
Недоступна
Открытие канала связи для журнала событий 📢
Открытие канала связи для журнала событий 📢
1
Задача
JAVA 25 SELF, 60 уровень, 3 лекция
Недоступна
Архитектор цифровой адресной книги 🗺️
Архитектор цифровой адресной книги 🗺️
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ioanna Polyak Уровень 42
10 декабря 2025
Во второй задаче Джавараш не принимает свое же решение 🗿