用jMolecules框架实现DDD应用开发

在本教程中,我们讨论将技术问题与业务逻辑分开以及明确声明这些技术概念的优势。我们发现 jMolecules 有助于实现这种分离,并根据所选的架构风格从架构角度实施最佳实践。

在本文中,我们将重新讨论关键的领域驱动设计 (DDD)概念,并演示如何使用jMolecules将这些技术问题表达为元数据。

我们将探讨这种方法如何使我们受益,并讨论 jMolecules 与 Java 和 Spring 生态系统中流行的库和框架的集成。

最后,我们将重点关注ArchUnit集成并学习如何使用它来在构建过程中强制遵循 DDD 原则的代码结构。

jMolecules 的目标
jMolecules 是一个库,它允许我们明确表达架构概念,从而提高代码清晰度和可维护性。作者的研究论文详细解释了该项目的目标和主要功能。

总而言之,jMolecules 帮助我们使领域特定代码摆脱技术依赖,并通过注释和基于类型的接口表达这些技术概念。

根据我们选择的方法和设计,我们可以导入相关的 jMolecules 模块来表达特定于该风格的技术概念。例如,以下是一些受支持的设计风格以及我们可以使用的相关注释:

  • 领域驱动设计 (DDD):使用@Entity、@ValueObject、@Repository和@AggregateRoot等注释
  • CQRS 架构:利用@Command、@CommandHandler和@QueryModel等注释
  • 分层架构:应用@DomainLayer、@ApplicationLayer和@InfrastructureLayer等注释
此外,工具和插件可以使用这些元数据来执行诸如生成样板代码、创建文档或确保代码库具有正确结构等任务。尽管该项目仍处于早期阶段,但它支持与各种框架和库的集成。

例如,我们可以导入Jackson和Byte-Buddy集成来生成样板代码,或者包含JPA和 Spring 特定模块来将 jMolecules 注释转换为其 Spring 等效项。

jMolecules 和 DDD
在本文中,我们将重点介绍 jMolecules 的 DDD 模块,并使用它来创建博客应用程序的域模型。首先,让我们将 jmolecumes -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 概念及其相关注释。

值对象
值对象是一个不可变的领域对象,它封装了属性和逻辑,而没有独特的标识。此外,值对象仅由其属性定义。

在文章和博客的上下文中,文章的 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;
    }
   
// getter
}

Java 记录本质上是不可变的,这使它们成为实现值对象的绝佳选择。让我们使用记录创建另一个@ValueObject来表示帐户用户名:

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

实体
实体与值对象的区别在于,它们拥有唯一身份并封装可变状态。它们表示需要独特标识的领域概念,并且可以随时间推移进行修改,同时在不同状态下保持其身份。

例如,我们可以将文章评论想象成一个实体:每条评论都会有一个唯一的标识符、一个作者、一条消息和一个时间戳。此外,实体可以封装编辑评论消息所需的逻辑:

@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 中,聚合是一组相关对象,它们被视为数据更改的单个单元,并且有一个对象被指定为集群内的根。聚合根封装了逻辑,以确保对自身和所有相关实体的更改发生在单个原子事务中。

例如,文章 将成为我们模型的聚合根。文章可以使用其唯一的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引用了另一个聚合:Author。它通过Username值对象来实现这一点,该值对象是Author聚合根的自然键。

存储库
存储库是提供访问、存储和检索聚合根的方法的抽象。从外部看,它们显示为聚合的简单集合。

由于我们将Article定义为聚合根,因此我们可以创建Articles类并用@Repository对其进行注释。此类将封装与持久层的交互并提供类似 Collection 的接口:

@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 注释,我们可以将代码中的架构概念定义为元数据。如前所述,这使我们能够与其他库集成以生成样板代码和文档。但是,在本文的范围内,我们将重点介绍如何使用archunit 和jmolecules-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  ,而不是使用文章的Slug。此外,我们可以有一个Email值对象,其中包含Author实体作为其字段之一,这也会违反 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 规则已经通过JMole​​culesDddRules定义。因此,我们只需要指定要为此测试验证的包:

@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 背后的想法

  • 明确表达架构概念,以便于阅读和编写代码。
  • 保持特定领域代码不受技术依赖。减少样板代码。
  • 自动生成文档并验证实施结构和架构。

目标

  1. 让开发人员的生活更轻松。
  2. 表达一段代码(一个包,类或方法)实现一个架构概念。
  3. 让人类读者能够轻松判断给定的一段代码属于哪种架构概念。
  4. 允许工具集成:[list=1]
  5. 代码增强。(工具示例:ByteBuddy 与 Spring 和 JPA 集成)。
  6. 检查架构规则。(工具示例:jQAssistant、ArchUnit)。

用例:生成技术样板代码

jMolecules注释和接口可用于生成表达某一目标技术中概念所需的技术代码。
可用的库

用例:验证并记录架构
以代码表达的 jMolecules 概念可用于验证源自概念定义的规则并生成文档。
可用的库