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
等注解
⚠️ 关键特性:这些元数据可被工具/插件用于生成样板代码、创建文档或验证代码结构。虽然项目仍处于早期阶段,但已支持多种集成方案。
例如:
- 导入Jackson和Byte-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 Boot和JPA等框架本身就遵循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
标识,负责管理content
、likes
和comments
状态:
@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注解可在代码中将架构概念定义为元数据。如前所述,这能集成其他库生成样板代码和文档。但本文重点是通过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
、值对象包含实体引用:
@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。