Как структурировать проекты на Spring/Spring Boot?
В данном тексте мы попробуем разобраться в самой спорной и холиварной теме – как разбивать на модули и как расположить файлы в проекте на Java и Spring.
Представим, что мы будем структурировать большой проект, который уже существует. Так как такие большие проекты обычно пишутся большой командой и защищены NDA, то за неимением такого мы возьмём небольшой проект и будем представлять, что он очень большой!
Итак, теперь мы имеем следующую структуру папок и файлов:
. ├── application/ │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── ru/example/ │ │ │ │ ├── controller/ │ │ │ │ │ ├── PersonController.java │ │ │ │ │ └── OrderController.java │ │ │ │ └── Main.java │ │ │ └── resources/ │ │ │ └── application.properties │ │ └── test/ │ │ └── ... │ └── pom.xml ├── database/ │ ├── src/ │ │ └── main/ │ │ └── java/ │ │ └── ru/example/dao/ │ │ └── impl/ │ │ ├── PersonDaoJdbc.java │ │ ├── OrderDaoJdbc.java │ │ ├── PersonDao.java │ │ └── OrderDao.java │ └── pom.xml ├── model/ │ ├── src/ │ │ └── main/ │ │ └── java/ │ │ └── ru/example/model/ │ │ ├── Person.java │ │ └── Order.java │ └── pom.xml ├── service/ │ ├── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── ru/example/service/ │ │ │ ├── impl/ │ │ │ │ ├── PersonServiceImpl.java │ │ │ │ └── OrderService.java │ │ │ ├── PersonService.java │ │ │ └── OrderService.java │ │ └── test/ │ │ └── ... │ └── pom.xml └── pom.xml
Возможно, вы узнали структуру вашего проекта!
Итак, начнём
Давайте попробуем упростить структуру с точки зрения современных практик и возможностей Spring Boot. Для начала посмотрим на разбиение на Maven-модули по слоям приложения. Существует такое понятие, как связность компонента (cohesion, не путать со связАнностью компонент — coupling).
Связности бывают разные: • случайная связность; • коммуникационная связность; • функциональная связность; • логическая связность; • процедурная связность; • последовательностная связность; • временнАя связность.
На самое деле, это — не лучшее деление для большого проекта. Чуть попозже мы поговорим про функциональную связность.
Существует несколько аргументов «за» такого деления: • можно на уровне зависимости Maven-модулей разделить архитектурные слои; • можно очень просто контролировать зависимости каждого слоя и используемые фреймворки.
Тем не менее существует несколько недостатков данного разделения: • собственно, функциональная связанность компонент; • большая сложность в дальнейшем разделить на микросервисы (они требуют функциональной связанности); • если вы используете Spring или Spring Boot, то начинаются проблемы с автоконфигурацией (Main -класс в одном модуле, а тестируемый класс – в другом); • да, и самое ужасное – application.properties находится в одном модуле, а использоваться могут в другом.
Объединим в один модуль. Итак, теперь мы имеем следующую структуру папок и файлов:
. ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── ru/example/ │ │ │ ├── controller/ │ │ │ │ ├── PersonController.java │ │ │ │ └── OrderController.java │ │ │ ├── service/ │ │ │ │ ├── impl/ │ │ │ │ │ ├── PersonServiceImpl.java │ │ │ │ │ └── OrderService.java │ │ │ │ ├── PersonService.java │ │ │ │ └── OrderService.java │ │ │ ├── model/ │ │ │ │ ├── Person.java │ │ │ │ └── Order.java │ │ │ ├── dao/ │ │ │ │ ├── impl/ │ │ │ │ │ ├── PersonDaoJdbc.java │ │ │ │ │ └── OrderDaoJdbc.java │ │ │ │ ├── PersonDao.java │ │ │ │ └── OrderDao.java │ │ │ └── Application.java │ │ └── resources/ │ │ └── application.properties │ └── test/ │ └── ... └── pom.xml
Обратим внимание, что каждый сервис покрыт отдельным интерфейсом.
Существует несколько аргументов «за» покрытие интерфейсами всех сервисов. Попробуем отойти от некоторых принципов в пользу более сжатого кода: • задать «контракт» для сервиса и соблюсти Dependency Inversion Principle. Собственно, с этим никак нельзя поспорить)). Но в современных средах разработки выделение интерфейса состоит из нажатия комбинации клавиш на клавиатуре. Эту операцию можно успеть сделать всегда, притом, что большинство интерфейсов всегда будут иметь только одного наследника; • добавить возможность создания JDK-proxy вместо CGLib-proxy при использовании AOP или некоторых проектов Spring. С этим тоже не поспоришь)). По скорости CGLib-proxy не хуже JDK dynamic proxy, правда, создаются чуть медленнее. Скорость поднятия контекста приложения – сомнительный параметр для оптимизации; • иметь возможность мокирования интерфейсов для написания Unit-тестов. А вот это не аргумент! C Mockito можно создать моки всех методов без наличия интерфейса.
Уберём ненужные интерфейсы.
. ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── ru/example/ │ │ │ ├── controller/ │ │ │ │ ├── PersonController.java │ │ │ │ └── OrderController.java │ │ │ ├── service/ │ │ │ │ ├── PersonService.java │ │ │ │ └── OrderService.java │ │ │ ├── model/ │ │ │ │ ├── Person.java │ │ │ │ └── Order.java │ │ │ ├── dao/ │ │ │ │ ├── PersonDao.java │ │ │ │ └── OrderDao.java │ │ │ └── Application.java │ │ └── resources/ │ │ └── application.properties │ └── test/ │ └── ... └── pom.xml
И, наконец, переструктурируем в функциональную связность:
. ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── ru/example/ │ │ │ ├── person/ │ │ │ │ ├── Person.java │ │ │ │ ├── PersonDao.java │ │ │ │ ├── PersonController.java │ │ │ │ └── PersonService.java │ │ │ ├── service/ │ │ │ │ ├── Order.java │ │ │ │ ├── OrderDao.java │ │ │ │ ├── OrderController.java │ │ │ │ └── OrderService.java │ │ │ └── Main.java │ │ └── resources/ │ │ └── application.properties │ └── test/ │ └── ... └── pom.xml
Вот и всё. Сравните теперь с исходным проектом!