DDD領域驅動工程落地實戰
我在公司對支付業務、結算業務、資金業務使用DDD進行領域建模的兩年,得到了許多好評,也面對過不少質疑,總體來說還是能收獲不少,這對團隊成員理解業務起著很大作用。近半年一直在研究DDD的落地實戰,如今已修得階段性成果,迫不及待與大家分享我的落地經驗。
DDD分為戰略設計與戰術設計。一般來說,領域建模是屬于戰略層的,而DDD工程落地是屬于戰術層的,兩者是否結合使用,視實際情況而定,比如傳統的MVC架構也能使用DDD進行領域建模,DDD架構最好是先做DDD領域建模。?
最新上線的一個微服務——內部交易中心,我們使用了DDD架構來落地,希望看完對大家有啟發。
一、工程架構分層理論
在工程落地之前,我們有必要先了解下主流的工程架構或架構思想都有哪些,對這些理論有所了解的,也可以直接跳過看下一個部分。
1、經典DDD四層架構
在該架構中,上層模塊可以調用下層模塊,反之不行。即:
- Interface ——> application | domain | infrastructure
- application ——> domain | infrastructure
- domain ——> infrastructure
分層作用:
- 用戶界面層/表現層:負責向用戶顯示解釋用戶命令
- 應用層:定義軟件要完成的任務,并且指揮協調領域對象進行不同的操作。該層不包含業務領域知識
- 領域層/模型層:系統的核心,負責表達業務概念,業務狀態信息以及業務規則。即包含了該領域(問題域)所有復雜的業務知識抽象和規則定義。該層主要精力要放在領域對象分析上,可以從實體,值對象,聚合(聚合根),領域服務,領域事件,倉儲,工廠等方面入手
- 基礎設施層:一是為領域模型提供持久化機制,當軟件需要持久化能力時候才需要進行規劃;二是對其他層提供通用的技術支持能力,如消息通信,通用工具,配置等的實現;
2、整潔架構思想
整潔架構(Clean Architecture)是由Bob大叔在2012年提出的一個架構模型,顧名思義,是為了使架構更簡潔。
依賴規則:用一組同心圓來表示軟件的不同領域。一般來說,越深入代表你的軟件層次越高。外圓是戰術是實現機制,內圓的是核心原則。
這條規則規定軟件模塊只能向內依賴,而里面的部分對外面的模塊一無所知,也就是內部不依賴外部,而外部依賴內部。同樣,在外面圈中使用的數據格式不應被內圈中使用,特別是如果這些數據格式是由外面一圈的框架生成的。
這樣做的最大好處是當系統的外部模塊不得不改變時(比如,替換已有的過時的數據庫系統),系統的內層模塊不需要做任何改變。
3、六邊形架構
六邊形架構(Hexagonal Architecture),又叫做端口適配器模式,是由Alistair Cockburn在2005年提出的。
六邊形架構將系統分為內部(內部六邊形)和外部,內部代表了應用的業務邏輯,外部代表應用的驅動邏輯、基礎設施或其他應用。內部通過端口和外部系統通信,端口代表了一定協議,以API呈現。
一個端口可能對應多個外部系統,不同的外部系統需要使用不同的適配器,適配器負責對協議進行轉換。這樣就使得應用程序能夠以一致的方式被用戶、程序、自動化測試、批處理腳本所驅動,并且,可以在與實際運行的設備和數據庫相隔離的情況下開發和測試。
4、菱形架構
作用于限界上下文的菱形對稱架構從領域驅動設計分層架構與六邊形架構中汲取了營養,通過對它們的融合形成了以領域為軸心的內外分層對稱結構。
內部以領域層的領域模型為主,外部的網關層則根據方向劃分為北向網關與南向網關。通過該架構,可清晰說明整個限界上下文的組成:
- 北向網關的遠程網關
- 北向網關的本地網關
- 領域層的領域模型
- 南向網關的端口抽象
- 南向網關的適配器實現
限界上下文以領域模型為核心向南北方向對稱發散,從而在邊界內形成清晰的邏輯層次,前端UI并未包含在限界上下文的邊界之內。每個組成元素之間的協作關系表現了清晰直觀的自北向南的調用關系。
5、CQRS
CQRS(Command Query Responsibility Segregation)意為命令查詢職責分離,它是一種與領域驅動設計 (DDD) 和事件溯源相關的架構模式。Greg Young在2010年創造了這個術語,CQRS的內容基于Bertrand Meyer的CQS設計模式。
CQRS架構將寫入和讀取分開,它提出了單獨的 API,一個專用于更改應用程序狀態的命令路由,另一個專用于返回有關應用程序狀態信息的查詢路由。
二、工程架構分層設計
基于各個架構有其自己的優缺點,我們結合公司的現狀,取其長避其短,融合一套適合自己的架構。
- 以經典DDD四層架構為骨架,其他優秀架構思想作指導
- CQRS命令/查詢職責分離,應用到DDD應用層,處理復雜操作/復雜查詢
- 整潔架構應用到DDD領域層與基礎設施層,接口與實現拆到不同層,把技術代碼與業務代碼分離
- 菱形架構指導我們,內部以領域層的領域模型為主,向南北兩個方法發散——北向網關(領域層以上)提供本地網關(如Controller、MQListener)與遠程網關(如API包);南向網關(領域層以下)負責端口抽象(如倉庫接口)與適配器實現(如外部API封裝實現)
- 公司的Base框架在dal包封裝了基礎CRUD接口,應用到數據訪問層內,作為領域層與基礎設施層的粘合劑,簡化鏈接
當然,任何事物有其兩面性,融合各個框架后,也有其優缺點——
優點:通過分離業務與技術代碼,有利于業務迭代升級維護;業務驅動而非技術/數據驅動,通過寫代碼就能積累一定的業務知識;將領域知識和技術知識分類,從而提高代碼的可重用性。
缺點:對從業人員業務分析能力較高,難以從經典MVC架構轉變過來;層級較多,寫代碼前需考慮清楚邏輯應該寫在哪一層;規則較多,沒有MVC架構靈活,不適用于簡單業務系統;學習成本與轉移成本比較高,需要對DDD有更好的理解和更長的設計時間(資金組踐行DDD領域建模2年)。
三、工程代碼構建案例
看代碼之前我們先看下領域建模:
通過領域模型分析,內部交易中心分為內部調貨、規則中心、內部出入庫、內部銷售、內部采購這五大模塊,每一個模塊對應DDD就是一個聚合,所有聚合形成一個DDD的限界上下文(內部交易上下文),之前的文章提到,限界上下文就是我們劃分微服務的一個重要依據。
接下來,我們結合DDD架構圖與領域建模,看看工程代碼應該怎么放。
基于Maven的DDD工程,頂層結構我們按api、service劃分為兩個module。
api包的作用:
- api包的定位是跨服務的頂層契約,service包所有層都可以依賴api包
- api包定義了對外透出的枚舉/常量、入參、出參、API接口等,為了方便使用api類,feign層不作業務劃分
- api包只定義契約不寫業務邏輯,避免因業務邏輯變更引發的api包升級
service包的作用:
- service包是工程的頂層實現,DDD四層架構在service包體現
- Application程序入口與DDD的四層處于同一目錄
此外,針對service包還有另一種主流的module劃分方式——直接把service包的api、application、domain、infrastructure作為四個獨立的module,優點是能通過pom依賴的方式來限制層與層之間的依賴,開發人員能在編碼階段發現依賴問題及時修正,但缺點也明顯——不夠靈活,工程也會變得較重。
1、接入層(api)
接入層又叫用戶接入層,主流用interface或api命名,基于包默認按字母排序的原因,我建議使用“api”來命名接入層,但要注意,service包的api層與api包是不同的作用。
- 接入層是很薄的一層,負責直接對接前端請求或feign實現(facade里的Controller)、數據轉換(assembler),入參/出參等契約類(request/response)統一定義在頂層的api包
- Controller負責對數據做前置校驗,具體業務邏輯則交給應用服務或領域服務實現,可直接調用應用服務方法或領域方法
- 業務劃分在接入層不明顯,更多是基于前端模塊進行劃分Controller,且業務復雜時必然存在領域交叉,故facade下沒有再細分業務包
- assembler數據轉換負責處理復雜的數據轉換,簡單的數據轉換可顯式調用工具類的轉換方法
2、應用層(application)
應用層主要作用是業務編排、轉發、校驗等,處理跨聚合、領域事件邏輯,復雜操作/復雜查詢也在此層體現(CQRS)。
- 應用服務AppService是一種簡單邏輯封裝,接入層無法直接調用領域層拿到結果的,可在此層編排封裝聚合方法
- 應用層可依賴領域層,但不可依賴接入層,所以傳參進應用層要么是基礎類型,要么在接入層assembler做一層轉換,要么入參出參定義在api包
- 事件一般情況是跨聚合或跨服務的,所以事件定義在應用層,在應用層處理事件的發布/訂閱
- 接入層可直接調用領域層,不經過應用層
3、領域層(domain)
領域層或稱為模型層,系統的核心,負責表達業務概念、業務狀態信息以及業務規則,包含了該領域所有復雜的業務知識抽象和規則定義。
- 領域層只表達業務,不寫技術代碼,在業務上不依賴其他層
- 領域聚合以業務來命名包,聚合內包含該聚合下所有模型(DO對象)、倉庫接口、領域服務
- 領域模型model是領域聚合下的業務核心模型,以XxxDO命名,依舊采用貧血模型,只包含少量原子性操作,不包含跨模型數據處理、持久化操作等
- 倉庫使用repository命名,領域層只定義倉庫接口,不寫倉庫實現
- 領域工廠factory與設計模式里的工廠模式不同,領域工廠主要負責領域對象的復雜構建,如領域對象生成、屬性填充等,由于存在跨聚合的情況,所以factory包并不在聚合內,與領域聚合同層級
- 外部API接口、外部框架代碼做一層淺封裝,放在external聚合包下,以ExXxxService命名接口,實現類還是在基礎設施層,起接口防腐作用
因為領域建模最終體現在領域層內,在我們建模時就要考慮領域層的代碼如何寫。
- 領域建模時只表達核心屬性與核心行為
- 聚合內跨多個模型的復雜業務邏輯,寫在領域服務內
- 領域模型的方法只寫原子性的操作,但不包括CRUD持久化操作
一些難點:
- 無法實現模型的“所建即所得”,復雜代碼無法通過領域模型的簡單幾個方法表達完整
- 模型只能表達核心的業務行為,所謂的充血模型在落地時可能更多地拆分到領域工廠、領域服務、應用服務中實現
4、基礎設施層(infrastructure)
基礎設施層作為工程的基礎設施使用,編寫與業務無關的代碼,如技術框架、工具類,此外還有一個重要的功能,要寫倉庫的實現類、外部服務的實現類。
- 基礎設施層的倉庫(repository)實現了領域層定義的倉庫接口,數據訪問層(dao)也定義在倉庫下,數據庫實體(PO對象)定義在entity,以XxxPO或XxxEntity命名,這里遵循了公司框架的命名方式,使用了XxxEntity
- param是比較特殊的一層,該類一般定義查詢數據庫的參數。基于公司的Base框架,repository定義接口時依賴了param對象,按道理領域層不應依賴基礎設施層(DIP原則),但Param又跟PO對象息息相關,所以把param對象放在了基礎設施層
- 基于Clean架構原則,其他框架性代碼、工具類、配置類都放在基礎設施層,業務代碼與技術代碼分離后,萬一升級技術代碼,對業務代碼做最少改動
我們再來看一下全貌:
通過實際案例,總結以下重要幾點:
- 領域層是業務最核心的一層,聚合之間的邊界需要劃分清晰,而接入層、應用層涉及跨聚合,基礎設施層關注倉儲實現與技術框架,所以我們只在領域層劃分業務包,對應領域建模按聚合劃分邊界,并定義領域模型的倉儲接口
- 充血模型建模,貧血模型落地,把核心業務行為按需劃分到領域模型(原子性、非持久化)、領域工廠(構建模型)、領域服務(跨模型)或應用服務(跨聚合、事件)中
- 使用接口分離業務代碼與技術性代碼,當業務迭代時,修改領域層和基礎設施層的倉庫實現即可;當技術框架升級時,修改基礎設施層即可,不至于把業務代碼也修改一遍,減少出錯成本
DDD工程落地考慮的是代碼的歸類劃分問題,重點在于業務邊界的識別、業務和技術代碼的解耦。寫代碼前需要考慮清楚不同的代碼應該寫到哪里,結合前人優秀的工程架構思路與公司當前的技術架構,整合一套靈活的、適合我們自己的DDD,不能照搬,更不能為了DDD而DDD。
四、疑難分析
1、用充血模型還是貧血模型?
其實除了常見的充血模型、貧血模型,還有不常用的失血模型、脹血模型,區別如下:
- 失血模型:只包含Getter/Setter的純數據類,一般不會有這種設計
- 貧血模型:包含模型屬性、Getter/Setter與非持久化的原子領域邏輯,持久化邏輯放在業務層(如Service類)
- 充血模型:比貧血模型多了持久化操作與絕大多數業務邏輯,實例化時會拿到很多不一定需要的關聯模型
- 脹血模型:只有領域對象與DAO兩層,在領域邏輯上封裝事務
基于現有的Spring框架,以及個人以往的代碼編寫經驗,在代碼落地層面還是以貧血模型進行較恰當。
2、放應用服務還是領域服務?
應用服務在應用層,領域服務在領域層,我怎么知道業務代碼該放哪里?
應用服務的作用:
- 負責展現層與領域層之間的協調,協調業務對象來執行特定的應用程序任務(編排業務)
- 放相對靈活的代碼邏輯,易于編排
- 操作粒度較大,事務管理在此處理
領域服務的作用:
- 負責表達業務概念,業務狀態信息以及業務規則,是業務軟件的核心
- 放相對原子性的核心代碼,封裝性與復用性強
- 操作粒度較細,不管理事務,領域模型不應該意識到事務的存在
其實,難點在于識別業務代碼,考驗我們對業務的理解程度與思考程度,如果可以顯然預料到未來會發生明顯的變化,則應該在設計之初更靈活地設計好;如果對未來的變化把握并不清晰或不確定,滿足當前業務需求即可。
我們無法避免過度設計還是設計不足,但如果架構合理,代碼清晰,改起來成本不會特別大。這里提倡開發者盡量多與領域專家(業務人員或產品經理)溝通,以把握代碼未來的走向。
3、特殊代碼如何歸類?
除了常規的簡單業務代碼,還涉及到復雜業務代碼拆分到不同類的問題,最典型的是運用設計模式。
- 工廠模式:根據不同條件生成相應對象,常見領域工廠、領域服務、應用服務內
- 策略模式:根據不同條件執行相應邏輯,定義一個策略接口和多個策略實現,常見領域服務、應用服務內
- 觀察者模式:使用發布/訂閱模式代替,可運用基礎設施層的SpringEvent來解耦代碼
- 責任鏈模式:拆分復雜業務邏輯到各個責任鏈類執行,常見領域服務、應用服務內
原則上,核心邏輯在哪一層拆就放在哪一層,避免代碼散落到各處。
五、一些經驗
?DDD領域建模三大步:劃分邊界、統一語言、組織模型。
DDD工程落地四大步:整合框架思想、確定劃分思路、模型代碼映射、特殊代碼歸類。?
以上,從DDD領域建模到DDD工程落地實戰已全篇完結,也歡迎大家一起來探討,如需要DDD工程的Demo也可以聯系我。
我相信80%的技術面試官都會對DDD這塊感興趣,如果你也掌握了DDD,其實就多掌握了一種面向RMB編程。