手把手教你落地 DDD

導讀

本文將從日常的三層架構出發,精煉推導出自己的應用架構,並且將這個應用架構實現爲 Maven Archetype,最後使用 Archetype 創建一個簡單的 CMS 項目作爲本文的落地案例。

01 前言

在今年的敏捷團隊建設中,我通過 Suite 執行器實現了一鍵自動化單元測試。Juint 除了 Suite 執行器還有哪些執行器呢?由此我的 Runner 探索之旅開始了!

常見的 DDD 實現架構有很多種,如經典四層架構、六邊形(適配器端口)架構、整潔架構(Clean Architecture)、CQRS 架構等。架構無優劣高下之分,只要熟練掌握就都是合適的架構。本文不會逐個去講解這些架構,感興趣的讀者可以自行去了解。

本文將帶領大家從日常的三層架構出發,精煉推導出我們自己的應用架構,並且將這個應用架構實現爲 Maven Archetype,最後使用我們 Archetype 創建一個簡單的 CMS 項目作爲本文的落地案例。

需要明確的是,本文只是給讀者介紹了 DDD 應用架構,還有許多概念沒有涉及,例如實體、值對象、聚合、領域事件等,如果讀者對完整落地 DDD 感興趣,可以詳細關注作者的開源項目《Thinking-in-DDD》,這個開源項目是筆者領域驅動設計實踐的總結,GitHub 鏈接爲:https://github.com/feiniaojin/Thinking-in-DDD

02  應用架構演化

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網絡獲取最新模板,當獲取到模板後進行模板加載,加載階段會將產物轉換爲視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕。     

很多項目是基於三層架構的,其結構如圖:

圖 1. 三層架構結構示意

既然說三層架構,爲什麼還畫了一層 Model 呢?因爲 Model 只是簡單的 Java Bean,裏面只有數據庫表對應的屬性,有的應用會將其單獨拎出來作爲一個 Maven Module,但實際上可以合併到 DAO 層。

接下來開始對這個三層架構進行抽象精煉。

2.1  第一步 數據模型與 DAO 層合併

爲什麼數據模型要與 DAO 層合併呢?

首先,數據模型是貧血模型,數據模型中不包含業務邏輯,只作爲裝載模型屬性的容器;

其次,數據模型與數據庫表結構的字段是一一對應的,數據模型最主要的應用場景就是 DAO 層用來進行 ORM,給 Service 層返回封裝好的數據模型,供 Service 獲取模型屬性以執行業務;

最後,數據模型的 Class 或者屬性字段上,通常帶有 ORM 框架的一些註解,跟 DAO 層聯繫非常緊密,可以認爲數據模型就是 DAO 層拿來查詢或者持久化數據的,數據模型脫離了 DAO 層,意義不大。

2.2  第二步 Service 層抽取業務邏輯

下面是一個常見的 Service 方法的僞代碼,既有緩存、數據庫的調用,也有實際的業務邏輯,整體過於臃腫,要進行單元測試更是無從下手。

public class Service {
    @Transactional
    public void bizLogic(Param param) {
        checkParam(param);//校驗不通過則拋出自定義的運行時異常
        Data data = new Data();//或者是mapper.queryOne(param);
        data.setId(param.getId());
        if (condition1 == true) {
            biz1 = biz1(param.getProperty1());
            data.setProperty1(biz1);
        } else {
            biz1 = biz11(param.getProperty1());
            data.setProperty1(biz1);
        }
        if (condition2 == true) {
            biz2 = biz2(param.getProperty2());
            data.setProperty2(biz2);
        } else {
            biz2 = biz22(param.getProperty2());
            data.setProperty2(biz2);
        }
        //省略一堆set方法
        mapper.updateXXXById(data);
    }
}

這是典型的事務腳本的代碼:先做參數校驗,然後通過 biz1、biz2 等子方法做業務,並將其結果通過一堆 Set 方法設置到數據模型中,再將數據模型更新到數據庫。

由於所有的業務邏輯都在 Service 方法中,造成 Service 方法非常臃腫,Service 需要了解所有的業務規則,並且要清楚如何將基礎設施串起來。同樣的一條規則,例如 if(condition1=true),很有可能在每個方法裏面都出現。

專業的事情就該讓專業的人幹,既然業務邏輯是跟具體的業務場景相關的,那就想辦法把業務邏輯提取出來,形成一個模型,讓這個模型的對象去執行具體的業務邏輯。這樣 Service 方法就不用再關心裏面的 if/else 業務規則,只需要通過業務模型執行業務邏輯,並提供基礎設施完成用例即可。

將業務邏輯抽象成模型,這樣的模型就是領域模型。

要操作領域模型,必須先獲得領域模型,但此時先不管領域模型怎麼得到,假設是通過loadDomain方法獲得的。通過 Service 方法的入參,調用loadDomain方法得到一個模型,讓這個模型去做業務邏輯,最後執行的結果也都在模型裏,再將模型回寫數據庫。當然,怎麼寫數據庫的也先不管,假設是通過saveDomain方法。

Service 層的方法經過抽取之後,將得到如下的僞代碼:

public class Service {
    public void bizLogic(Param param) {
        //如果校驗不通過,則拋一個運行時異常
        checkParam(param);
        //加載模型
        Domain domain = loadDomain(param);
        //調用外部服務取值
      SomeValue someValue=this.getSomeValueFromOtherService(param.getProperty2());
        //模型自己去做業務邏輯,Service不關心模型內部的業務規則
        domain.doBusinessLogic(param.getProperty1(), someValue);
        //保存模型
        saveDomain(domain);
    }
}

根據代碼,這時已經將業務邏輯抽取出來了,領域相關的業務規則封閉在領域模型內部。此時 Service 方法非常直觀,就是獲取模型、執行業務邏輯、保存模型,再協調基礎設施完成其餘的操作。

抽取完領域模型後工程的結構如下圖:

圖 2. 三層架構抽取業務模型後的架構圖

2.3  第三步 維護領域對象生命週期

在上一步中,loadDomainsaveDomain 這兩個方法還沒有得到討論,這兩個方法跟領域對象的生命週期息息相關。

關於領域對象的生命週期的詳細知識,讀者可以自行學習瞭解。

不管是 loadDomain 還是 saveDomain,一般都要依賴於數據庫,所以這兩個方法對應的邏輯,肯定是要跟 DAO 產生聯繫的。

保存或者加載領域模型,可以抽象成一種組件,通過這種組件進行封裝模型加載、保存的操作,這種組件就是 Repository。

注意,Repository 是對加載或者保存領域模型(這裏指的是聚合根,因爲只有聚合根纔會有 Repository)的抽象,必須對上層屏蔽領域模型持久化的細節,因此其方法的入參或者出參,一定是基本數據類型或者領域模型,不能是數據庫表對應的數據模型。

以下是 Repository 的僞代碼:

public interface DomainRepository {
    void save(AggregateRoot root);
    AggregateRoot load(EntityId id);
}

接下來要考慮在哪裏實現DomainRepository。既然 DomainRepository 與底層數據庫有關聯,但是現在 DAO 層並沒有引入 Domain 這個包,DAO 層自然無法提供 DomainRepository 的實現,於是初步考慮是不是可以將 DomainRepository 實現在 Service 層。

但是,如果在 Service 中實現 DomainRepository,勢必需要在 Service 層操作數據模型:查詢出來數據模型再封裝爲領域模型、或者將領域模型轉爲數據模型再通過 ORM 保存,這個過程不該是 Service 層關心的。

因此,決定在 DAO 層直接引入 Domain 包,並在 DAO 層提供 DomainRepository 接口的實現,DAO 層查詢出數據模型之後,封裝成領域模型供 DomainRepository 返回。

這樣調整之後,DAO 層不再向 Service 返回數據模型,而是返回領域模型,這就隱藏了數據庫交互的細節,也就把 DAO 層換個名字稱之爲 Repository。

現在,項目的架構圖是這樣的了:

圖 3. 三層架構演化第三步的架構圖

由於數據模型屬於貧血模型,自身沒有業務邏輯,並且只有 Repository 這個包會用到,因此將之合併到 Repository 中,接下來不再單獨列舉。

2.4  第四步 泛化抽象

在第三步中,架構圖已經跟經典四層架構非常相似了,再對某些層進行泛化抽象。

Repository 倉儲層其實屬於基礎設施層,只不過其職責是持久化和加載聚合,所以,將 Repository 層改名爲 infrastructure-persistence,可以理解爲基礎設施層持久化包。

之所以採取這種 infrastructure-XXX 的格式進行命名,是由於 Infrastructure 可能會有很多的包,分別提供不同的基礎設施支持。

例如:一般的項目,還有可能需要引入緩存,就可以再加一個包,名字叫infrastructure-cache

對於外部的調用,DDD 中有防腐層的概念,將外部模型通過防腐層進行隔離,避免污染本地上下文的領域模型。使用入口(Gateway)來封裝對外部系統或資源的訪問(詳細見《企業應用架構模式》,18.1 入口(Gateway)),因此將對外調用這一層稱之爲infrastructure-gateway

注意:Infrastructure 層的門面接口都應先在 Domain 層定義,其方法的入參、出參,都應該是領域模型(實體、值對象)或者基本類型。

Controller 層其實就是用戶接口層,即 User Interface 層,在項目簡稱 ui。當然了可能很多開發者會覺得叫 UI 好像很彆扭,認爲 UI 就是 UI 設計師設計的圖形界面。

Controller 層的名字有很多,有的叫 Rest,有的叫 Resource,考慮到這一層不只是有 Rest 接口,還可能還有一系列 Web 相關的攔截器,所以一般也可以稱之爲 Web。因此,我們將其改名爲 ui-web,即用戶接口層的 Web 包。

同樣,可能會有很多的用戶接口,但是他們通過不同的協議對外提供服務,因而被劃分到不同的包中。

如果有對外提供的 RPC 服務,那麼其服務實現類所在的包就可以命名爲 ui-provider

有時候引入某個中間件會同時增加 Infrastructure 和 User Interface。

例如,如果引入 Kafka 就需要考慮一下,如果是給 Service 層提供調用的,例如邏輯執行完發送消息通知下游,那麼就再加一個包infrastructure-publisher;如果是消費 Kafka 的消息,然後調用 Service 層執行業務邏輯的,那麼就可以命名爲 ui-subscriber

至此,Service 層目前已經沒有業務邏輯了,業務邏輯都在 Domain 層去執行了,Service 只是協調領域模型、基礎設施層完成業務邏輯。

所以,把 Service 層改名爲 Application Service 層。

經過第四步的抽象,其架構圖爲:

圖 4. 三層架構演化第四步的架構圖

2.5  第五步 完整的包結構

繼續對第四步中出現的包進行整理,此時還需要考慮一個問題,啓動類應該放在哪裏?

由於有很多的 User Interface,所以啓動類放在任意一個 User Interface 中都不合適,放置在 Application Service 中也不合適,因此,啓動類應該存放在單獨的模塊中。又因爲 application 這個名字被應用層佔用了,所以將啓動類所在的模塊命名爲 launcher,一個項目可以存在多個 launcher,按需引用 User Interface。

加入啓動包,就得到了完整的 maven 包結構。

包結構如圖所示:

圖 5. 三層架構演化第五步的架構圖

至此,DDD 項目的整體結構基本講完了。

2.6  精煉後的思考

在經過前面五步精煉得到這個架構圖中,經典四層架構的四層都出現了,而且長得跟六邊形架構也很像。這是爲什麼呢?

其實,不管是經典四層架構、還是六邊形架構,亦或者整潔架構,都是對系統應用的描述,也許描述的側重點不一樣,但是描述的是同一個事物。既然描述的是同一個事物,長得像纔是理所當然的,不可能只是換一個描述方式,系統就從根本上發生了改變。

對於任何一個應用,都可以看成 “輸入 - 處理 - 輸出” 的過程。

“輸入” 環節:通過某種協議對外暴露領域的能力,這些協議可能是 REST、可能是 RPC、可能是 MQ 的訂閱者,也可能是 WebSocket,也可能是一些任務調度的 Task;

“處理” 環節:處理環節是整個應用的核心,代表了應用具備的核心能力,是應用的價值所在,應用在這個環節執行業務邏輯,貧血模型由 Service 執行業務處理,充血模型則是由模型進行業務處理。

“輸出” 環節,業務邏輯執行完成之後將結果輸出到外部。

不管採用的什麼架構,其描述的應用的核心都是這個過程,不必生搬硬套非得用什麼應用架構。

正如《金剛經》所言:一切有爲法,如夢幻泡影,如露亦如電,應作如是觀;凡所有相,皆是虛妄;若見諸相非相,即見如來。

03  ddd-archetype

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網絡獲取最新模板,當獲取到模板後進行模板加載,加載階段會將產物轉換爲視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕。

3.1 Maven Archetype 介紹

Maven Archetype 是一個 Maven 插件,可以幫助開發人員快速創建項目的基礎結構,大大減少開發人員在創建項目時所需的時間和精力,並且可以確保項目結構的一致性和可重用性,從而提高代碼質量和可維護性。

前文在介紹 DDD 應用架構時,對項目的結構進行了介紹。將項目分爲多個 Maven Module,如果每個項目都手工創建一次,是比較繁瑣的工作,也不利項目結構的統一。

使用 Maven Archetype 創建 DDD 項目初始化的腳手架,使其在初始化時完整實現上文第五步的應用架構。

3.2 ddd-archetype 的使用

3.2.1 項目介紹

ddd-archetype 是一個 Maven Archetype 的原型工程,將其克隆到本地之後,可以安裝爲 Maven Archetype,幫助使用者快速創建 DDD 項目腳手架。

項目鏈接:https://github.com/feiniaojin/ddd-archetype

3.2.2 安裝過程

以下將以 IDEA 爲例展示 ddd-archetype 的安裝使用過程,主要過程是:

克隆項目-->archetype:create-from-project-->install-->archetype:crawl

3.2.3 克隆項目

將項目克隆到本地:

git clone https://github.com/feiniaojin/ddd-archetype.git

直接使用主分支即可,然後使用 IDEA 打開該項目

圖 6.IDEA 打開項目示意

3.2.4 archetype:create-from-project

配置打開 IDEA 的run/debug configurations窗口,如下:

圖 7. 配置打開 IDEA 的run/debug configurations窗口示意

選擇add new configurations,彈出以下窗口:

圖 8. 選擇add new configurations彈出窗口示意

其中,上圖中 1~4 各個標識的值爲:

標識1 - 選擇 "+" 號;

標識2 - 選擇 "Maven";

標識3 - 命令爲:

archetype:create-from-project -Darchetype.properties=archetype.properties

注意,在 IDEA 中添加的命令默認不需要加 mvn

標識4 - 選擇ddd-archetype的根目錄

以上配置完成後,點擊執行該命令。

3.2.5 install

上一步執行完成且無報錯之後,配置install命令。

圖 9. 配置install命令示意

其中,上圖中 1~2 各個標識的值爲:

標識1 - 值爲install

標識2 - 值爲上一步運行的結果,路徑爲:

ddd-archetype/target/generated-sources/archetype

install配置完成之後,點擊執行。

3.2.6 archetype:crawl

install執行完成且無報錯,接着配置archetype:crawl命令。

圖 10. 配置archetype:crawl命令示意

其中,標識 1 中的值爲:

archetype:crawl

配置完成,點擊執行即可。

3.3 使用 ddd-archetype 初始化項目

圖 11. 創建項目時,點擊manage catalogs

圖 12. 將本地的 maven 私服中的archetype-catalog.xml加入到 catalogs 中

添加成功,如下:

圖 13. 添加成功示意

圖 14. 創建項目時操作示意

圖 15. 項目創建完成後示意

04   代碼案例

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網絡獲取最新模板,當獲取到模板後進行模板加載,加載階段會將產物轉換爲視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目

本文提供了配套的代碼案例,該案例使用 DDD 和本文的應用架構實現了簡單的 CMS 系統。案例項目採用前後端分離的方式,因此有後端和前端兩個代碼庫。

4.1 後端

後端項目使用本文的ddd-archetype創建,實現了部分 CMS 的功能,並落地部分 DDD 的概念。

GitHub 鏈接:https://github.com/feiniaojin/ddd-example-cms

圖 16. 後端項目使用本文的ddd-archetype創建

實現的 DDD 概念有:實體、值對象、聚合根、Factory、Repository、CQRS。

技術棧:

無外部中間件依賴 ,clone 到本地即可編譯運行,非常方便。

4.2 前端

前端項目基於vue-element-admin開發,詳細安裝方式見代碼庫的 README。

GitHub 鏈接:https://github.com/feiniaojin/ddd-example-cms-front

圖 17.README 中的詳細安裝方式

4.3 運行截圖

圖 18. 運行截圖

05   總結以及進一步學習

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網絡獲取最新模板,當獲取到模板後進行模板加載,加載階段會將產物轉換爲視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目

本文通過對貧血三層架構進行精煉,推導出適合落地的應用架構,並且將之實現爲 Maven Archetype 以應用到實際開發。然而,應用架構只是落地 DDD 的基礎,要完整落地 DDD,讀者還需要進一步掌握限界上下文、上下文映射、充血模型、實體、值對象、領域服務、Factory、Repository 等知識點,希望本文能爲讀者拋磚引玉,打開學習領域驅動設計的大門。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/TU5G2o_HOctjWvuyOEK_tA