用jMolecules框架實現DDD應用開發

banq發表於2024-08-20

在本教程中,我們討論將技術問題與業務邏輯分開以及明確宣告這些技術概念的優勢。我們發現 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, <font>"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<i>
}

Java 記錄本質上是不可變的,這使它們成為實現值物件的絕佳選擇。讓我們使用記錄建立另一個@ValueObject來表示帳戶使用者名稱:

@ValueObject
record Username(String value) {
    public Username {
        Assert.isTrue(value != null && !value.isBlank(), <font>"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;
    <font>// constructor, getters<i>
    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;
  
    <font>// constructor, getters<i>
    void comment(Username user, String message) {
        comments.add(new Comment(user, message));
    }
    void publish() {
        if (status == Status.DRAFT || status == Status.HIDDEN) {
           
// ...other logic<i>
            status = Status.PUBLISHED;
        }
        throw new IllegalStateException(
"we cannot publish an article with status=" + status);
    }
    void hide() {
/* ... */<i> }
    void archive() {
/* ... */<i> }
    void like(Username user) {
/* ... */<i> }
    void dislike(Username user) {
/* ... */<i> }
}

我們可以看到,文章實體是包含評論實體和一些值物件的聚合的根。聚合不能直接引用其他聚合中的實體。因此,我們只能透過文章根與評論實體進行互動,而不能直接從其他聚合或實體進行互動。

此外,聚合根可以透過其識別符號引用其他聚合。例如,Article引用了另一個聚合:Author。它透過Username值物件來實現這一點,該值物件是Author聚合根的自然鍵。

儲存庫
儲存庫是提供訪問、儲存和檢索聚合根的方法的抽象。從外部看,它們顯示為聚合的簡單集合。

由於我們將Article定義為聚合根,因此我們可以建立Articles類並用@Repository對其進行註釋。此類將封裝與持久層的互動並提供類似 Collection 的介面:

@Repository
class Articles {
    Slug save(Article draft) {
        <font>// save to DB<i>
    }
    Optional<Article> find(Slug slug) {
       
// query DB<i>
    }
    List<Article> filterByStatus(Status status) {
       
// query DB<i>
    }
    void remove(Slug article) {
       
// update DB and mark article as removed<i>
    }
}

執行 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 { <font>// <-- entities and aggregate roots should have an identifier<i>
    private Article latestArticle;
// <-- aggregates should not directly reference other aggregates<i>
    @ValueObject
    record Email(
      String address,
      Author author
// <-- value objects should not reference entities<i>
    ) {
    }
 
   
// constructor, getter, setter<i>
}

現在,讓我們編寫一個簡單的ArchUnit測試來驗證程式碼的結構。主要的 DDD 規則已經透過JMole​​culesDddRules定義。因此,我們只需要指定要為此測試驗證的包:

@AnalyzeClasses(packages = <font>"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) {
    }
    <font>// constructor, getters, setters<i>
}

jMolecules 背後的想法

  • 明確表達架構概念,以便於閱讀和編寫程式碼。
  • 保持特定領域程式碼不受技術依賴。減少樣板程式碼。
  • 自動生成文件並驗證實施結構和架構。

目標

  1. 讓開發人員的生活更輕鬆。
  2. 表達一段程式碼(一個包,類或方法)實現一個架構概念。
  3. 讓人類讀者能夠輕鬆判斷給定的一段程式碼屬於哪種架構概念。
  4. 允許工具整合:[list=1]
  5. 程式碼增強。(工具示例:ByteBuddy 與 Spring 和 JPA 整合)。
  6. 檢查架構規則。(工具示例:jQAssistant、ArchUnit)。
<ul>

用例:生成技術樣板程式碼

jMolecules註釋和介面可用於生成表達某一目標技術中概念所需的技術程式碼。
可用的庫

  • Spring、Data JPA、Data MongoDB、Data JDBC 和 Jackson 整合 ——使得使用 jMolecules 註釋的程式碼在這些技術中開箱即用。

用例:驗證並記錄架構
以程式碼表達的 jMolecules 概念可用於驗證源自概念定義的規則並生成文件。
可用的庫

  • jQAssistant 外掛 — 用於驗證適用於不同架構風格、DDD 構建塊、CQRS 和事件的規則。還可以根據程式碼庫中可用的資訊建立 PlantUML 圖。
  • ArchUnit 規則 ——允許驗證 DDD 構建塊之間的關係。
  • Spring Modulith—— 支援檢測 jMolecules 元件、DDD 構建塊和事件,以用於模組模型和文件目的(有關更多資訊,請參閱https://docs.spring.io/spring-modulith/docs/current-SNAPSHOT/reference/html/#documentation/<a> [Spring Modulith 文件])。


 

相關文章