В этой статье мы повторим основные концепции предметно-ориентированного проектирования (Domain-Driven Design, DDD) и покажем, как с помощью jMolecules можно выразить технические аспекты в виде метаданных.
Мы изучим, какие преимущества дает такой подход, а также обсудим интеграцию jMolecules с популярными библиотеками и фреймворками из экосистемы Java и Spring.
Наконец, мы посмотрим на интеграцию с ArchUnit и узнаем, как использовать его для проверки, что структура исходников соответствует принципам DDD.
Цель jMolecules
jMolecules - это библиотека, которая позволяет явно выражать архитектурные концепции, улучшая читаемость кода. В статье авторов содержится подробное объяснение целей проекта и основных функций.
Если кратко, jMolecules позволяет избежать лишних зависимостей в коде бизнес-логики и выразить технические концепции через аннотации и интерфейсы.
В зависимости от подхода и архитектуры, которую мы выбираем, мы можем импортировать соответствующий модуль JMolecules для выражения технических концепций, специфичных для этой архитектуры. Например, вот некоторые поддерживаемые стили архитектуры и связанные с ними аннотации, которые они предоставляют:
DDD - вы можете отмечать аннотациями
@Entity
,@ValueObject
,@Repository
,@AggregateRoot
CQRS - аннотации
@Command
,@CommandHandler
,@QueryModel
Архитектурные слои - аннотации
@DomainLayer
,@ApplicationLayer
,@InfrastructureLayer
Кроме того, эти метаданные могут затем использоваться инструментами и плагинами для таких задач, как генерация кода, генерация документации или обеспечение корректности структуры. Несмотря на то, что проект находится на ранней стадии, он поддерживает интеграции с различными библиотеками.
Например, мы можем подключить интеграции Jackson, ByteBuddy и JPA и транслировать аннотации jMolecules в их аналоги для Spring.
jMolecules и DDD
В этой статье мы сосредоточимся на модуле DDD и используем его для создания доменной модели приложения для ведения блога. Во-первых, давайте добавим зависимости jmolecules-starter-ddd и jmolecules-starter-test в pom.xml:
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-starter-ddd</artifactId>
<version>0.21.0</version>
</dependency>
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-starter-test</artifactId>
<version>0.21.0</version>
<scope>test</scope>
</dependency>
В приведенных ниже примерах кода мы заметим сходство между аннотациями jMolecules и других фреймворков. Это происходит потому, что такие фреймворки, как Spring Boot или JPA, также следуют принципам DDD. Давайте кратко рассмотрим некоторые ключевые концепции DDD и связанные с ними аннотации.
Value Objects
Value object - это неизменяемый объект предметной области, который инкапсулирует атрибуты и логику, не имея собственной идентичности. Такие объекты определяются исключительно своими атрибутами.
В контексте приложения для ведения блога slug статьи неизменен и может справиться с собственной валидацией при создании. Это делает его идеальным кандидатом для @ValueObject
:
@ValueObject
class Slug {
private final String value;
public Slug(String value) {
Assert.isTrue(value != null, "Article's slug cannot be null!");
Assert.isTrue(value.length() >= 5, "Article's slug should be at least 5 characters long!");
this.value = value;
}
// getters
}
Record в java по своей природе неизменны, что делает их отличным выбором для реализации объектов-значений. Давайте используем record для создания еще одного объекта-значения для представления имени пользователя:
@ValueObject
record Username(String value) {
public Username {
Assert.isTrue(value != null && !value.isBlank(), "Username value cannot be null or blank.");
}
}
Сущности
Сущности отличаются от value objects тем, что они обладают идентичностью и изменяемым состоянием. Они представляют концепции предметной области, которые требуют отдельной идентификации и могут быть изменены с течением времени при сохранении своей идентичности в разных состояниях.
Например, мы можем представить себе комментарий в качестве сущности: каждый комментарий имеет уникальный идентификатор, автора, текст сообщения и дату/время. Кроме того, сущность может инкапсулировать логику, необходимую для редактирования сообщения комментария:
@Entity
class Comment {
@Identity
private final String id;
private final Username author;
private String message;
private Instant lastModified;
// constructor, getters
public void edit(String editedMessage) {
this.message = editedMessage;
this.lastModified = Instant.now();
}
}
Агрегаты
В DDD агрегаты представляют собой группы связанных объектов, которые рассматриваются как единый блок при изменении и имеют выделенный объект, называемый корень агрегата. Корень агрегата инкапсулирует логику, которая гарантирует, что изменения всех связанных объектов происходят в рамках одной транзакции.
Например, в нашей модели сущность Статья (Article) будет корнем агрегата. Статья может быть идентифицирована с использованием уникального slug и будет отвечать за управление её содержанием, лайками и комментариями:
@AggregateRoot
class Article {
@Identity
private final Slug slug;
private final Username author;
private String title;
private String content;
private Status status;
private List<Comment> comments;
private List<Username> likedBy;
// constructor, getters
void comment(Username user, String message) {
comments.add(new Comment(user, message));
}
void publish() {
if (status == Status.DRAFT || status == Status.HIDDEN) {
// ...other logic
status = Status.PUBLISHED;
}
throw new IllegalStateException("we cannot publish an article with status=" + status);
}
void hide() { /* ... */ }
void archive() { /* ... */ }
void like(Username user) { /* ... */ }
void dislike(Username user) { /* ... */ }
}
Как мы видим, сущность Article является корнем агрегата, который включает в себя сущности комментариев и некоторые value objects. Агрегаты не могут непосредственно ссылаться на сущности из других агрегатов. Таким образом, мы можем взаимодействовать с сущностью комментарий только через корень (статью), а не напрямую из других агрегатов или сущностей.
Кроме того, агрегаты должны ссылаться на другие агрегаты только через их идентификаторы. Например, статья ссылается на другой агрегат - автора. Это происходит через объект Username, который является естественным ключом сущности автор.
Репозитории
Репозитории - это абстракции, которые предоставляют методы доступа, хранения и получения корней агрегатов. Снаружи они выглядят как простые коллекции агрегатов.
Поскольку мы определили статью как корень агрегата, мы можем создать класс Articles и аннотировать его @Repository
. Этот класс будет инкапсулировать взаимодействие со слоем базы данных и обеспечивать интерфейс для доступа к данным:
@Repository
class Articles {
Slug save(Article draft) {
// save to DB
}
Optional<Article> find(Slug slug) {
// query DB
}
List<Article> filterByStatus(Status status) {
// query DB
}
void remove(Slug article) {
// update DB and mark article as removed
}
}
Соблюдение принципов DDD
Использование аннотаций jmolecules позволяет определить архитектурные концепции в нашем коде в виде метаданных. Как обсуждалось ранее, это позволяет нам интегрироваться с другими библиотеками для генерации кода и документации. В данный момент сосредоточимся на обеспечении соблюдения принципов DDD с использованием Arch-unit и jmolecules-archunit:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-archunit</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
Давайте создадим новый корень агрегата и намеренно нарушим некоторые правила DDD. Например, мы можем создать класс автора без идентификатора, который ссылается на статью непосредственно (в виде прямой ссылки на сущность) вместо использования slug. Кроме того, у нас может быть value object для представления электронной почты, который включает в себя ссылку на сущность автора, что также нарушало бы принципы DDD:
@AggregateRoot
public class Author { // <-- entities and aggregate roots should have an identifier
private Article latestArticle; // <-- aggregates should not directly reference other aggregates
@ValueObject
record Email(
String address,
Author author // <-- value objects should not reference entities
) {
}
// constructor, getter, setter
}
Теперь давайте напишем простой тест на archunit, чтобы провалидировать структуру исходников. Основные правила DDD уже определены через JMoleculesDddRules. Так что нам просто нужно указать пакеты, которые мы хотим проверить в этом тесте:
@AnalyzeClasses(packages = "com.baeldung.dddjmolecules")
class JMoleculesDddUnitTest {
@ArchTest
void whenCheckingAllClasses_thenCodeFollowsAllDddPrinciples(JavaClasses classes) {
JMoleculesDddRules.all().check(classes);
}
}
Если мы попытаемся запустить тест, то увидим следующие нарушения:
Author.java: Invalid aggregate root reference! Use identifier reference or Association instead!
Author.java: Author needs identity declaration on either field or method!
Author.java: Value object or identifier must not refer to identifiables!
Давайте исправим ошибки и убедимся, что наш код проходит проверки:
@AggregateRoot
public class Author {
@Identity
private Username username;
private Email email;
private Slug latestArticle;
@ValueObject
record Email(String address) {
}
// constructor, getters, setters
}
Заключение
В этой статье мы обсудили разделение технических аспектов и бизнес-логики, а также преимущества явного объявления этих технических концепций. Мы выяснили, что jMolecules помогает достичь такого разделения и обеспечивает соблюдение лучших архитектурных практик в зависимости от выбранного архитектурного стиля.
Кроме того, мы вновь повторили ключевые концепции DDD и использовали агрегаты, сущности, объекты-значения и репозитории для построения доменной модели сайта для блогов. Понимание этих концепций помогло нам создать предметную область, а интеграция jMolecules с ArchUnit позволила убедиться в соблюдении лучших практик DDD.