領域驅動設計:DDD

最近幾年,DDD(Domain-Driven Design)領域驅動設計越來越受技術人重視,不論是傳統企業數字化轉型項目中,還是平時的軟件架構設計中,抑或是各種技術文章中,DDD 都是討論的熱點。特別是微服務架構和雲原生技術時代之後,DDD 對於構建統一語言、系統應用架構設計、微服務劃分等都是非常重要的手段。今天我們就來看一看 DDD 的一些通用知識、核心設計方法、分層架構、常見的設計原則、以及一些通用的例子。

DDD 是什麼

DDD 是 Eric Evans 在 2003 年出版的《領域驅動設計:軟件核心複雜性應對之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一書中提出的具有劃時代意義的重要概念。DDD 通過統一語言、領域模型、領域劃分等一系列手段來降低軟件複雜度。

大家對面向對象設計(OO)不會陌生,其實 DDD 就是在面相對象設計基礎上的擴展和延伸,DDD 基於面向對象分析技術進行了分層規劃,對軟件開發過程全生命週期使用語言的統一,並強調業務與技術相結合的一種過程。DDD 利用面向對象的特性(封裝、多態)有效地化解複雜性,相比之下,傳統的數據驅動設計(比如基於 J2EE 或事務性編程模型)只關係數據,除了簡單的 setter/getter 方法外,不包含業務邏輯,業務邏輯都是以過程式的代碼寫在 Service 中。這種方式極易上手,但隨着業務的發展,系統也很容易變得混亂複雜。

我們先來看一個例子,Eric Evans 在書中有一個貨物運輸的例子,經過一系列分析和討論,最後得到的領域模型如下:

  1. 一個 Cargo(貨物)涉及多個 Customer(客戶,如託運人、收貨人、付款人),每個 Customer 承擔不同的角色;

  2. Cargo 的運送目標已指定,即 Cargo 有一個運送目標;

  3. 由一系列滿足 Specification(規格)的 Carrier Movement(運輸動作)來完成運輸目標。

從中我們可以看出,Eric 沒有從用戶的角度去描述領域模型,而是以領域內的相關事物爲出發點,考慮這些事物的本質關聯及其變化規律的。比如,他將貨物作爲整個領域的中心,而客戶等角色都是圍繞其周圍的角色;同時貨物有一個確定的目標,經過一系列的運輸動作到達目的地。而一般我們基於過程或者基於數據爲中心,往往只停留在分析的表面,而領域驅動強調對核心的領域進行建模,挖掘需求的本質。

DDD vs 數據驅動設計

圖片

我們在進一步看看 DDD 與傳統數據驅動的不同,這一點往往技術同學不太關注。一般情況下,很多同學喜歡從數據出發,先梳理 ER 圖,設計數據庫表,編寫 DAO,然後進行業務實現。數據驅動主要是貧血模型,業務邏輯散落在大量的方法中。數據模型設計關注的是數據存儲,數據儘量不要冗餘,控制表數量不膨脹,並重視數據的擴展性。這樣的設計不是不可以,系統小規模時還好,當系統越來越複雜時,開發時間指數增長,維護成本很高。

而領域驅動設計從領域出發,分析領域內模型及其關係,並進行領域建模,設計核心業務邏輯,進而再進行技術細節實現。DDD 優先考慮領域概念的業務語義表達,具有獨立業務概念的東西會盡量抽象成一個內聚的領域對象。領域對象不僅僅有屬性,還有該有的行爲。其核心思想是通過領域驅動設計方法定義領域模型,從而確定業務和應用邊界,保證業務模型與代碼模型的一致性。

在 DDD 中,領域模型和數據模型是解耦的,以領域模型爲出發點,進一步體現到核心的實體、值對象、聚合、領域服務上,強調對業務語義的顯性化表達,而不是數據的存儲和數據之間的關係,這是 “領域驅動設計” 和“數據驅動設計”之間顯著的區別。在軟件工程的早期,爲了彌補二者之間的差異,先驅者們嘗試用(Object Relationship Mapping,ORM)工具,但工具是無法進行映射的,而 DDD 的核心並不是用什麼工具,而是如何整體的建模分析的過程。所以,大家在進行 DDD 時,一定要跳出數據模型優先的束縛,不要讓領域模型被數據模型綁架。

DDD 的好處

DDD 有非常多的好處,這裏我重點強調其中 2 點:

如何進行 DDD

DDD 包含戰略設計、戰術設計、技術實現三個部分。戰略設計側重於高層次、宏觀上去劃分領域和限界上下文等,而戰術設計則關注使用 DDD 的核心概念,比如實體、值對象、領域服務、聚合、領域事件等來細化上下文,通過領域模型來表達業務。技術實現主要通過分層架構來隔離領域模型代表的業務邏輯和技術細節。

圖片

DDD 戰略設計

戰略設計主要設計如下幾個核心概念:通用語言、領域、限界上下文等。

(1)通用語言 Ubiquitous Language

技術同學經常用 DAO、DTO 等技術視角來對分類對象,而這與業務視角討論的業務術語經常對不起來,導致彼此互相聽不懂,溝通效率低。DDD 通過一套面向對象的分類方法,從領域出發,實現軟件開發過程中各個角色和環境的 “統一語言”。通用語言建立的過程並非容易,因爲技術人員和領域專家在溝通過程中存在天然屏障的,過程中需要各方充分溝通。

(2)領****域 Domain

領域是來確定範圍和邊界的,同時爲了降低業務理解和系統實現的複雜度,DDD 會將領域進一步劃分成更小粒度,也就是子域。所以,領域可以進一步分爲核心域、通用子域、支持子域等概念。領域中的核心是領域模型(Domain Model),領域模型通過提煉領域對象,定義領域對象之間的關係,屬性和行爲,屬於 DDD 的核心產物。

(3)限界上下文 Bounded Context

領域幫助我們對系統進行拆分,而限界上下文幫助回答域之間的邊界以及如何交互,限界上下文是一個顯式的概念性邊界,領域模型都存在於這個邊界之內,出了這個邊界就不能確保這個含義。DDD 中有一個對限界上下文的形象比喻,“細胞之所以會存在,是因爲細胞膜定義了什麼在細胞內、什麼在細胞外,並且確定了什麼物質可以通過細胞膜。” 同時,限界上下文的交互方法有多種,在實際工作中,目前用的比較廣的是防腐層和統一協議。

圖片

這裏就領域和限界上下文做一個簡單例子,比如一個購物車訂單支付下單的例子,購物車進行在線的支付授權,訂單處理下單過程,並觸發支付域的付款結算。這裏我們簡化整個建模的過程,假設已經抽象出購物車域、支付域、訂單域(當然通常購物車也可以包含在訂單域),領域內部我們進行了核心模型的聚合,圖中只展示了核心的 Cart、Payment、Order。領域之間我們通過限界上下文進行交互,因爲購物車域支付域密切相關,需要等待支付授權,我們通過防腐層 ACL 進行關聯;而訂單下單和付款動作相對接耦,我們通過領域事件(後文會介紹),在訂單已下單後,觸發支付的付款動作。

DDD 戰術設計

DDD 戰術層面是領域建模最核心的一步,是分析實體、值對象、領域服務、聚合、領域事件等核心概念。

(1)實體 Entity

實體是一個具有唯一身份標識的對象,並且可以在相當長的一段時間內持續地變化。另外,實體具有可變性,內部一般是充血模型,會封裝包含這個實體相關的業務邏輯。以訂單 Order 爲例,Order 有訂單實體,下單、發貨和退單等行爲,但傳統經典方式是將這些行爲放到另一個服務 OrderService 中,而不是 Order 對象之中。

(2)值對象 Value Object

值對象是隻關心屬性的對象,沒有標識符。值對象一般依附於實體而存在, 是實體屬性的一部分,而非獨立存在,是不變的。值對象沒有唯一標識,值對象功能單一,一般是貧血模型。以 Order 爲例,訂單下的送貨地址 Address 就是典型的值對象。Address 並不是隨着 Order 產生而產生,相對不變,也不需要一個單獨標示。

(3)領域服務 Domain Service

領域中的一些概念不太適合建模爲對象,它們本質上是一些操作,一些動作,往往會涉及到多個領域對象,這就是領域服務。識別領域服務有幾個關鍵特徵:領域服務體現的行爲不屬於任何實體和值對象的;被執行的操作涉及到領域中的其他的對;操作是無狀態的。以 OrderDelivery 訂單發貨爲例,需要 Order 和履約兩種實體之間通過一定的業務邏輯,並確保事務,可以作爲 DomainService。

(4)聚合 Aggregate

聚合的組成由兩部分,一部分稱爲根實體,是聚合中的特定實體;另一部分描述一個邊界,定義聚合內部都有什麼。一個域的聚合可以理解成整個域中核心實體和值對象的組合,並通過一個根實體進行代表,同時要滿足固定規則。比如訂單域可能有很多實體,如訂單、子訂單、訂單明細、地址、物流信息、支付信息等,而我們聚合爲訂單域後,這些實體都聚焦在一起,並由 Order 這個實體作爲聚合根對外交互。

(5)領域事件 Domain Event

領域事件表示領域中所發生的重要事件,事件發生後通常會導致進一步的業務操作,或者在系統其它地方引起反應。領域事件非常重要,我們系統設計過程中經常需要接耦,技術同學一般通過 MQ 方式進行;架構同學可能使用 Event-Driven Architecture EDA 的方式,而 Serverless 架構中核心的就是基於事件編程,這一切的核心就是對領域事件的設計,不過當前大部分系統 Event 設計比較隨性,也導致 Event 濫用和無用情況發生,而領域事件是對我們很好的方向指引。比如訂單例子中,訂單已下單後,會觸發庫存凍結、支付狀態更新、物流同步等,都是對系統事件的良好接耦設計。

下面通過一張圖就前面提到的一些核心概念做一個小的總結:

圖片

DDD 常用分析方法

DDD 一些常用的分析方法主要有用例分析法,四色建模法,以及事件風暴法。這裏我們介紹兩種方法。

(1)用例分析法

用例分析是比較通用的領域建模方法,可以在比較傳統需求調研過程中再結合領域模型的設計思路進行,核心是通過業務需求、場景流程等梳理用例,進而規劃領域模型。編寫用例時要避免使用技術術語,而應該用最終用戶或者領域專家的語言。進而,我們可以基於用例的方法,根據語義來整理用例,進而整理領域模型,大體可以按照如下方式:

+  收集用例

+  提取實體

+  提取屬性

+  添加關聯

+  完善模型

舉個例子,比如讓你設計一個電商客服系統,一個典型的 User Story 可能是 “客戶 A 反饋訂單問題,客服說你留個電話,有進一步處理結果我會通知你”,這裏

·  客戶 A 是客戶。

·  電話是客戶的屬性。

·  客服包含了客服系統,客服員工兩個關鍵對象。

·  訂單、工單肯定也是關鍵領域對象;

·  通知這個動詞暗示可能有領域事件,或技術上通過觀察者模式實現。

·  進而梳理之間的關係,比如一個客戶可能有多個工單,是 O2M 關係等。

(2)事件風暴法

前文我們提到了領域事件,而基於領域事件的建模方法就是事件風暴。事件風暴與頭腦風暴類似,可以快速分析複雜業務領域,完成領域建模的目標。事件風暴是事件驅動設計的典型代表,是一種羣體建模技術。關注如下元素:

+   事件:發生了什麼事情,產生了什麼結果,用桔黃色表示。

+   屬性:事件的輸入、輸出,是對時間的細化描述

+   命令:某個動作的發起者,可能是人,外部事件,定時器等,用藍色表示。

+   領域:領域的聚合,內聚,低耦合,聚合內保證數據一致性,用黃色表示。

簡單理解就是誰在何時基於什麼(輸入)做了什麼(命令)產生了什麼(輸出)影響了什麼(事件),最後聚合成怎樣的(領域)。前文我們提到了 “訂單已下單” 這種事件,這裏主要提供一下關鍵圖例。

圖片

舉個例子,比如角色 “用戶” 進行了命令 “提交訂單” 產生事件“訂單已創建”,或者角色“運營人員” 進行了命令“同步庫存” 產生事件“庫存已變化”。

對於事件 Event,也有一些注意事項:

+  Event 命名:Domain Name + 動詞的過去式 + Event。比如 OrderCreatedEvent

+  Event 內容:Enrichment(payload 中放 data),Query-Back(通過回調拿到更多的 data)

+  Event 管理:通過 MQ 等保存所有的 Events,並提供良好的好的 Event 查詢和回溯。

+  Event 處理:事件構建和發佈、事件數據持久化、事件總線、消息中間件、事件接收和處理等。

DDD 技術實現 - 分層架構

DDD 在具體落地實時過程中,強調四層分層結構,將前面提到的核心概念進行有效的整合,各層的職能定義如下:

圖片

展示層:負責向前臺顯示信息和解釋用戶命令,完成前端界面邏輯。展示層的組件實現用戶與應用交互的功能,也可以叫用戶接口層,面向前端應用,提供應用級別入口完成用戶操作。比如對前端展示(web,wireless,wap)的路由和適配,也相當於 MVC 中的 controller。

應用層:負責展現層與領域層之間的協調,負責獲取輸入,組裝上下文,參數校驗,調用領域層做業務處理,對領域層組件進行簡單封裝,例如事務、執行單位操作、調用應用程序的任務。

+   領域層:是領域驅動設計的核心,包含了前面提到的核心概念,如領域實體、值對象、領域服務、聚合以及它們之間的關係,負責表達業務概念、業務狀態信息以及業務規則,具體表現形式就是領域模型。

+   基礎設施層:向其他層提供通用的技術能力,爲應用層傳遞消息(API 網關等),爲領域層提供持久化機制(如數據庫資源、中間件交互、緩存、MQ 消息、搜索引擎、文件系統)等,屏蔽技術底座能力以及其他通用的工具類服務。

圖片

除了比較經典的 4 層分層架構,DDD 還有一種鬆散分層理念,認爲應該推平分層架構,平面型分層得以出現。平面型分層架構通過劃分內部和外部,系統由內而外圍繞領域模型進行展開,領域部分位於最內層,應用程序包含領域模型和業務邏輯,對於外部而言,通過各種適配器進行上下文集成,包括數據持久化、APP、Web 應用、第三方數據集成等。這樣很好地做到了業務邏輯和用戶界面的交錯問題,實現了前後端隔離,依賴關係明確,由外向內依賴。

當然,工具層面,也有一些比較好的開源框架,用來支持 DDD 分層架構的相關腳手架產出,因爲 DDD 的核心是緘默分析的過程,對於工具,每個工具定位不同,同時大家在具體的項目中 DDD 的目的也有不同,這裏我們不做推薦介紹,也歡迎感興趣的同學留言和討論。

DDD 設計的幾點建議

1、設計原則:儘可能 follow SOLID 原則,即單一職責原則(SRP),開閉原則(OCP),里氏替換原則(LSP),接口隔離原則(ISP),依賴倒置原則(DIP),以及 23 個設計模式。

2、關注依賴問題:比如無循環依賴、依賴倒置等。同時,注意一些常用的服務設計原則,比如服務無狀態、重試冪等性。

3、重視建模本身:DDD 不僅是一種編程語言,也是一種思維模式,核心是業務需求理解,統一語言建模,不要過於糾結用什麼工具和具體框架,適合自己的纔是最好的,同時基於六邊形框架設計,做到框架無關。

4、模型數量:實體、服務、事件等並不是越多越好,要注意核心模型的複用,邏輯內聚,複用性高,減少接口數量。同時儘量減少跨域的調用,做好限界上下文的處理。

5、DDD 並不是萬能的:如果系統並沒有複雜的業務邏輯,可以用一般的面向數據的架構或者事務腳本等模式即可,殺雞不用牛刀。DDD 的學習、改造和兼容成本需要我們關注。

6、事務:DDD 的聚合中有描述事務,一個事務內只操作一個聚合實例。建議最終一致性,並在應用層聲明事務。
7、CQRS: Command Query Responsibility Segregation 用於將領域模型與查詢功能進行分離,讓一些複雜的查詢擺脫領域模型的限制,以更爲簡單的 DTO 形式從 DB 中直接向前臺展現查詢結果,分離不同的數據存儲結構,讓開發者按照查詢的功能與要求更加自由的選擇數據存儲引擎,比如通過異步處理、大數據搜索等方式。

8、服務編排:DDD 是非常重要的應用架構設計方法,對於業務架構,還需要進一步的梳理業務流程和業務能力,可能會涉及到流程進一步編排,這就要做好架構的分層,讓 DDD 更加發揮自己的特長,讓更適合做流程編排的比如流程引擎來做服務編排。

9、微服務與中臺:DDD 是微服務和中臺中非常重要的分析方法,微服務進一步通過技術進行落地,而中臺是更上層的全局架構設計思想,互爲補充。

電商 DDD 例子

前文中,我們介紹了不少 DDD 在戰略、戰術、技術層面的特性,這裏我們來綜合看一個例子。這是一個簡單的電商例子,只展示了非常核心的幾個領域,每個領域也只挑選了個別領域實體和值對象,主要給大家一個大體的全局感覺。

圖片

領域:用戶域、會員域、營銷域、訂單域、商品域、庫存域,這裏沒有畫全,比如還會有支付、結算、物流、履約等。

+   聚合:這裏強調一下聚合根,比如會員域的會員,營銷域的活動人羣,用戶域的賬號等。

領域模型:圖中白色的框框,不同域各不相同,比如商品中需要進一步細化處 SKU、SPU。

值對象:圖中灰色的框框,比如商品域中的屬性、規格、價格。

領域服務:比如會員創建用戶,商品購物車關聯,會員參與活動等。

領域事件:比如會員商品已添加,商品庫存已同步等。

需要注意的是,在真正的 DDD 實踐中,會遠遠大於上圖中的內容,比如每個域的分析構建都需要大量的設計細節(比如會員域和支付域,大家可以參考我公衆號單獨的文章),其中一些領域事件、限界上下文也並沒有畫的非常規範。但我認爲 DDD 最大的魅力正是分析梳理的過程,也希望讀者朋友,自己動手畫一畫你認爲的電商核心領域模型,或者你所在的業務行業的領域模型,與我交流。如果大家想進一步瞭解 DDD,也建議大家再仔細閱讀一下《領域驅動設計:軟件核心複雜性應對之道》《實現領域驅動設計》

小結

DDD 是我們進行系統建模分析的利器,好處有很多,比如統一語言、業務領域知識沉澱、邊界清晰設計、關注點分離等。DDD 通過限界上下文、值對象、聚合、領域服務、領域事件等核心概念,根據用例分析、事件風暴等方法進行設計,並基於分層架構進行技術落地開發。當然 DDD 不是萬能的,有較高的學習門檻,需要整個團隊形成統一認識,也需要相應的規範和技術落地。因此 DDD 在使用時要時刻注意選擇它的初衷,不用侷限在相關的原則等條條框框,如果 DDD 的相關知識框架對大家在模型構建和相互協同中有一定幫助,我認爲就非常有意義。

作者簡介

王思軒,博士

北美計算機博士,哈工大本碩,7 年北美和歐洲海外經歷,10 餘篇國際學術論文。曾任阿里云云原生解決方案架構師,多年從事於雲計算和架構設計工作。致力於數字化轉型、架構理論、中臺、雲原生、新零售技術、技術產品化、領域建模、全球化技術等領域。

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