В этой статье мы повторим основные концепции предметно-ориентированного проектирования (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.
ссылка на оригинал статьи https://habr.com/ru/articles/909206/
Добавить комментарий