Как стать автором
Обновить

Валидируем концепции DDD с помощью jMolecules

Время на прочтение7 мин
Количество просмотров346
Автор оригинала: Emanuel Trandafir

В этой статье мы повторим основные концепции предметно-ориентированного проектирования (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.

Теги:
Хабы:
0
Комментарии0

Публикации

Работа

Java разработчик
191 вакансия

Ближайшие события