使用 jMolecules 進行 DDD
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 模組,並使用它來建立部落格應用程式的領域模型。首先,讓我們將[jmolecumes-starter-ddd](https://mvnrepository.com/artifact/org.jmolecules.integrations/jmolecules-starter-ddd)
和[jmolecules-starter-test](https://mvnrepository.com/artifact/org.jmolecules.integrations/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 概念及其相關註釋。
3.1.值對象
值對像是一個不可變的域對象,它封裝了屬性和邏輯,沒有明確的標識。此外,值物件僅由其屬性定義。
在文章和部落格的上下文中,文章的 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
來表示帳號Username
:
@ValueObject
record Username(String value) {
public Username {
Assert.isTrue(value != null && !value.isBlank(), "Username value cannot be null or blank.");
}
}
3.2.實體
實體與值物件的不同之處在於它們擁有唯一的識別並封裝可變狀態。它們代表需要獨特標識的領域概念,並且可以隨著時間的推移進行修改,同時在不同的狀態下保持其標識。
例如,我們可以將文章評論想像為一個實體:每個評論都有一個唯一的識別碼、作者、訊息和時間戳記。此外,實體可以封裝編輯評論訊息所需的邏輯:
@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.聚合根
在 DDD 中,聚合是一組相關對象,它們被視為資料變更的單一單元,並且有一個物件被指定為叢集內的根。聚合根封裝了邏輯,以確保對其自身和所有相關實體的變更發生在單一原子事務中。
例如,一篇Article
將是我們模型的聚合根。一篇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
根與Comment
實體交互,而不能直接從其他聚合或實體交互。
此外,聚合根可以透過其標識符引用其他聚合。例如, Article
引用了不同的聚合: Author
。它透過Username
值物件來完成此操作,該物件是Author
聚合根的自然鍵。
3.4.儲存庫
儲存庫是提供存取、儲存和檢索聚合根的方法的抽象。從外部看,它們看起來像是簡單的聚合集合。
由於我們將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
}
}
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
,而不是使用文章的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 規則已透過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 上取得。