1. 概述

本文将重新审视领域驱动设计(DDD)的核心概念,并演示如何使用jMolecules将这些技术关注点表达为元数据。

我们将探讨这种方法的实际收益,并讨论jMolecules与Java和Spring生态系统中主流库/框架的集成方案。

最后重点介绍ArchUnit集成,学习如何在构建过程中强制执行符合DDD原则的代码结构。

2. jMolecules的核心目标

jMolecules是一个允许我们显式表达架构概念的库,能显著提升代码清晰度和可维护性。作者的研究论文详细说明了项目目标和主要特性。

核心价值:jMolecules帮助我们将领域特定代码与技术依赖解耦,并通过注解和类型化接口表达技术概念

根据选择的设计风格,我们可以导入对应jMolecules模块表达特定概念。例如:

  • 领域驱动设计(DDD):使用@Entity@ValueObject@Repository@AggregateRoot等注解
  • CQRS架构:利用@Command@CommandHandler@QueryModel等注解
  • 分层架构:应用@DomainLayer@ApplicationLayer@InfrastructureLayer等注解

⚠️ 关键特性:这些元数据可被工具/插件用于生成样板代码、创建文档或验证代码结构。虽然项目仍处于早期阶段,但已支持多种集成方案

例如:

  • 导入JacksonByte-Buddy集成生成样板代码
  • 使用JPA和Spring模块将jMolecules注解转换为Spring等效注解

3. jMolecules与DDD实践

本文重点使用jMolecules的DDD模块构建博客应用的领域模型。首先在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 BootJPA等框架本身就遵循DDD原则。下面回顾关键DDD概念及对应注解:

3.1 值对象(Value Objects)

值对象是封装属性和逻辑的不可变领域对象,没有独立身份标识。其属性值完全决定对象本身。

在博客场景中,文章的slug(URL友好标识符)不可变且能自校验,适合标记为@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;
    }

    // getter
}

Java records天然不可变,是实现值对象的绝佳选择。用record创建另一个@ValueObject表示账户用户名:

@ValueObject
record Username(String value) {
    public Username {
        Assert.isTrue(value != null && !value.isBlank(), "Username value cannot be null or blank.");
    }
}

3.2 实体(Entities)

实体与值对象的核心区别在于:实体具有唯一身份标识,封装可变状态。它们代表需要持久化识别的领域概念,可在不同状态修改中保持身份。

例如文章评论可设计为实体:每条评论有唯一ID、作者、消息和时间戳,且封装消息编辑逻辑:

@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();
    }
}

3.3 聚合根(Aggregate Roots)

DDD中聚合是作为数据变更单元的相关对象组,其中指定一个根对象。聚合根封装逻辑,确保自身及关联对象在单原子事务中完成变更

例如Article将作为模型聚合根,通过唯一slug标识,负责管理contentlikescomments状态:

@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根操作Comment实体,不能跨聚合直接访问。

此外,聚合根可通过标识符引用其他聚合。例如Article通过Username值对象(Author聚合的自然键)引用Author聚合。

3.4 仓储(Repositories)

仓储是提供访问、存储和检索聚合根方法的抽象。对外表现为简单的聚合集合。

由于Article是聚合根,创建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
    }
}

4. 强制执行DDD原则

使用jMolecules注解可在代码中将架构概念定义为元数据。如前所述,这能集成其他库生成样板代码和文档。但本文重点是通过archunitjmolecules-archunit强制执行DDD原则

<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规则的Author聚合根:缺少标识符、直接通过对象引用Article、值对象包含实体引用:

@AggregateRoot
public class Author { // <-- 实体和聚合根需要标识符
    private Article latestArticle; // <-- 聚合不应直接引用其他聚合

    @ValueObject
    record Email(
      String address,
      Author author // <-- 值对象不应引用实体
    ) {
    }
 
    // 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
}

5. 总结

本文探讨了业务逻辑与技术关注点的分离,以及显式声明技术概念的优势。我们发现jMolecules能实现这种分离,并根据所选架构风格强制执行架构最佳实践

此外我们重温了核心DDD概念,使用聚合根、实体、值对象和仓储构建博客网站领域模型。理解这些概念有助于创建健壮的领域模型,而jMolecules与ArchUnit的集成使我们能验证DDD最佳实践。

示例代码已托管在GitHub


原始标题:DDD with jMolecules | Baeldung