作者 | 祁兮
談論到 DDD,我們會聊事件風暴,會聊限界上下文,會聊六邊形架構,會聊實體值對象。這些概念各不相同,相關的概念也很不一樣,但都屬于DDD的范疇。見過了很多DDD的討論和工作坊,我發現大家唇槍舌劍無法達成一致,往往是因為各自腦中的問題并不相同。
我嘗試在軟件設計領域,將這些問題劃分到幾個相互獨立的范疇,這可以幫助我和其他人討論,在明確范圍內可以更好的交流。
一種比較經典的方式是劃分為戰略設計和戰術設計。由于領域模型設計復雜度也很高,所以我又把領域模型設計從戰術設計中劃分出來,形成單獨的范疇,以便更好的討論。
下面我將討論這三個范疇的概念和方法。
一、DDD戰略設計
在這個范疇里,主要討論目標是復雜的業務需求。有多復雜呢?可能需要多個團隊分工合作,或者一個團隊分階段開發,需要被設計成多個獨立部署運行的服務,會有多個代碼庫。
這個范疇可以有很多名字,比如DDD戰略設計、進程間架構、微服務架構設計等。
為什么要分成多個部分?因為解決復雜問題的一個有效方法是將其分解為多個相對簡單的問題,然后分別解決。如果不進行分解,這個復雜問題往往會讓我們在解決過程中陷入困境,就算設計出了解決方案,也往往由于解決方案過于復雜導致團隊的認知超載。
1.劃分方法
既然戰略設計需要將整個業務需求分成多個部分,那么如何找到用于劃分的接縫呢?
我看到行業里有這樣一些方法:
(1) 限界上下文
在《領域驅動設計》中,Eric提出了限界上下文。從領域模型設計的角度,為了讓模型保持完整獨立和清晰,需要識別出限界上下文,讓其作為模型的邊界。在書中并沒有完善的識別方法,更多的是提出一些概念。限界上下文往往被用來輔助判斷接縫的正確性。
在一個限界上下文中,領域知識是相對完整的。
(2) 核心域
在《領域驅動設計》中,Eric提出了精煉及核心域。在模型中識別出最有價值的核心域,將其獨立出來。
由于只提到了核心域,所以這也不是一個完整的劃分的方法。我曾在如何劃分限界上下文博客中基于此方法上提出了一種分解問題域的方法。
(3) 事件風暴工作坊
事件風暴工作坊可能是最早用來指導劃分限界上下文的方法。
對前一步(事件風暴)產生的聚合進行分組,通過業務的內聚性和關聯度劃分邊界,結合限界上下文的定義進行判斷,并給出上下文名稱。
——[服務化設計階段路徑方案]
但是「業務的內聚性和關聯度」著實不是一個好的劃分依據。而事件風暴的創始人Alberto曾經提出過通過關鍵事件識別不同的階段進而識別限界上下文的方法,看上去是一個更加靠譜的方法。
(4) 8X Flow
8X Flow中提出了一套相對完整的劃分方法。首先定義「業務」和「領域」,然后將「業務」和「領域」劃分開來,接著基于合同將業務劃分成了不同的上下文,最終完成了劃分。
(5) 現代企業架構白皮書
現代企業架構白皮書提出通過職責類型劃分。流轉類識別不同的業務流程階段,規格類提取業務規則,視圖類專為統計報表而存在,配置類提供配置工具。
2.重新思考
我也嘗試過一些其他的劃分方法,比如通過時間階段劃分,通過使用者不同劃分,通過使用場景不同劃分,通過變化頻率不同劃分。這些方法和上面的一些方法都有些相似。
不好的劃分方法可能會導致分布式單體:每次變化不得不修改多個服務、每次部署必須同時部署多個服務,服務之間有非常多的通信,同一個團隊管理著多個服務,服務之間共享數據庫、同樣的代碼和模型。
也許我們可以總結出一些原則,來幫助我們驗證劃分是否合理。比如高內聚低耦合,比如服務有明確的邊界且能自治,可以獨立演進,比如盡可能減少對于其他服務的依賴。
二、DDD戰術設計
在戰略層面劃分好了服務后,我們來看看一個服務內部。
在這個范疇里,主要討論在一個服務內部,如何劃分和組織代碼。
和上一節類似,在代碼也有不同的職責;和上一節不同,對于代碼層面的劃分,已經有相對成熟的方法。
這個范疇可以有很多名字,比如DDD戰術設計、進程內架構、分層架構等。
需要指出的是,在一個服務內部,如果領域模型足夠復雜,在分離領域邏輯和技術實現細節前,也需要先按照模塊進行一次劃分,然后再按上述的領域邏輯和技術實現細節的方式劃分。相關討論可以參見前綴分包vs后綴分包。
1.劃分方法
(1)《領域驅動設計》中的分層架構
Eric在2003年提出的分層架構。和傳統的展示層+業務邏輯層+數據訪問層的三層架構相比多了一層,主要區別是將業務邏輯層分成了應用層和領域層。
圖片引自《領域驅動設計》第4章
其中「應用層」這個概念,也指明了它和領域層的區別:領域層專注表達領域概念,而應用層則在領域層之上,加入了諸如持久化概念和事務概念等軟件的典型概念,對外提供了滿足具體場景的功能。展示層則在應用層功能之上,定義了和外部系統通信的具體形式。
這里也將數據訪問層變成了基礎設施層。基礎設施層為其他層提供支撐其概念的具體技術實現。
(2) 六邊形架構
2005年六邊形架構(翻譯)又稱端口和適配器架構,從設計模式的視角將代碼劃分成了負責業務邏輯的「應用」和負責同外部系統交互的「適配器」。
圖片引自《六邊形架構》
在2013的IDDD中Vaughn將六邊形架構和DDD進行了結合,把「應用」又細分成了「應用程序」和「領域模型」。
圖片引自《實現領域驅動設計》第4章
2008年的洋蔥架構也是類似的。
六邊形架構從另外一個角度審視了一個理想架構,并將領域層放在中心,凸顯其核心地位。
(3) 整潔架構
Uncle Bob在2012提出了整潔架構,一般來說我們認為整潔架構的四層(四圈)和IDDD的六邊形架構基本是對應的,只是整潔架構將適配器劃分成了和框架耦合的「Frameworks & Drivers」層和負責內外層數據轉換的「Interface Adapters」層。
圖片引自《整潔架構》
整潔架構也用「用例」來描述業務實體之外的一層,對應于「應用層」,更明確的指明了這層的職責是實現各個用例。
比較有趣的是,整潔架構把Gateway接口放到了領域層之外的「用例層」。這使得領域層只關注于當前上下文的邏輯,而讓用例層負責和其他上下文/資源庫的協調和編排。
整潔架構也討論了如何處理框架和架構的關系。
(4) 清晰架構
2017年更有集DDD、洋蔥架構、整潔架構、CQRS于一體的清晰架構出現。
2.重新思考
以上的架構,指導每一個具體的業務功能分解來說是非常夠用的。然而在一個真實的項目中,除了每個具體功能的分層,其實還有一些對于平臺和框架的配置,這些其實要和每個業務功能的代碼有所區分,從代碼結構上獨立出來。
另外,每一層都會有一些可以復用的代碼。比如領域層的基礎的業務異常,應用層的事務處理,適配器層的HTTP客戶端。這些不只用于單個模塊或者單個服務,也可以用于多個服務;有些已經有三方工具,有些需要我們自己定義和封裝。
我看到很多項目對于以上兩類代碼并沒有區分,而是把一切不屬于其他層的代碼都放到了基礎設施層。讓可憐的基礎設施層逐漸變成了垃圾桶。
三、領域模型設計
在戰術層面劃分好架構后,我們來看看位于核心的領域模型。
在這個范疇里,主要討論基于面向對象技術,如何用領域模型來表達業務概念。
為什么要使用領域模型這種模式,而不是用Service+數據模型的模式呢?如果復雜的業務邏輯采用數據模型這種模式,那么Service里會存在大量的復雜的邏輯,代碼是很難維護的。而領域模型充分利用了面向對象技術的優勢,將復雜度轉變為職責明確的組件組合,讓各個組件相對簡單,來降低認知負載,提升可維護性。這就是設計的力量。
那為什么用面向對象技術呢?面向對象思想更加符合我們認知復雜問題的方式,并且現代編程語言都普遍支持面向對象,所以DDD選擇了面向對象技術。
1.關注點分離模式
在這個范疇里,主要還是使用《領域驅動設計》中的模式。我們以關注點分離的角度,來解析這些模式。
(1) 領域對象的生命周期類型
從生命周期的角度,「領域對象」分為這樣幾個類型:
- 和應用生命周期一致,應用啟動時被創建出來,應用關閉時才銷毀。比如《領域驅動設計》5.4.1中的「資金轉賬」。
- 在業務過程中被創建,會被保留一段時間,不隨著應用關閉銷毀。比如電商系統中的「訂單」。
- 在業務過程中被創建,在使用完成后即被銷毀。比如一些在對象之間傳遞的參數對象。
而在《領域驅動設計》的第5章,Eric也將領域對象劃分為了實體、值對象、領域服務這三個重要模式。這三個模式和生命周期是如何對應的呢?
對于類型1,和應用生命周期一致,就是領域服務這種模式。對于類型2,在業務過程中被創建,會被保留一段時間,對應于實體和值對象。而對于類型3,在業務過程中被創建隨即被銷毀,對應于值對象。
VALUE OBJECT 經常作為參數在對象之間傳遞消息。它們常常是臨時對象,在一次操作中被創建,然后丟棄。
——《領域驅動設計》 5.3 值對象
(2) 分離領域對象的創建、查詢、保存和使用
從生命周期角度,對于這三類領域對象的創建邏輯,可以使用Factory模式,將其封裝在Factory中。對于類型2的領域對象的保留及之后的查詢,可以使用Repository模式,將其模擬成一個集合從而進行存取操作。
Eric把Factory和Repository被歸為「支持對象」,以和其他用于表示模型的領域對象分開。
(3) 分離函數和命令
使用無副作用的函數模式,把沒有副作用的查詢邏輯提取出來,成為無副作用的函數,而讓有副作用的命令盡可能簡單。
基于同樣的理由,我也在考慮將有IO操作的邏輯提取出來,直接讓應用層調用,而不是和其他業務邏輯組合。
(4) 分離領域中的算法
使用Strategy模式,把業務邏輯中的變化點放到策略對象中,讓不同的實現可以互換,從而實現關注點分離。
(5) 分離領域中的規則
使用Specification模式,將領域中用于判斷是非的業務規則放到規格對象中。
(6) 分離做什么和怎么做
采用Intention-Revealing Interface和Cohesive Mechanism模式,把「做什么」和「怎么做」分離。讓釋意接口專注于表明意圖,方便調用方使用;讓內聚機制封裝實現細節,在釋意接口背后解決問題。
2.重新思考
我發現在OO BootCamp中得到的模型往往無法直接用于真實項目中,這讓我用新的角度重新學習和思考了領域模型。
在實際項目中,設計者往往過早陷入對于一些具體模式的識別,比如實體、聚合、領域服務,而忽略了如何設計一個可以表達領域概念的模型。我們應該基于領域概念設計領域模型,然后再采用合適的模式降低領域模型的復雜度,進一步增加領域模型的表達能力。
很多項目雖然也使用了以領域為核心的架構,但是設計者仍然是數據模型/貧血模型的思考方式,把大量領域邏輯放置在了萬能的Service中,讓領域概念隱藏在了冗長的過程代碼中,絲毫沒有享受到DDD帶來的收益。
軟件的核心是其為用戶解決領域相關的問題的能力。
——《領域驅動設計》 第一部分
在學習了讓我們眼花繚亂的眾多方法后,我們重新回到DDD的初衷,重新審視軟件設計和DDD之間的關系,讓DDD幫助我們提升軟件設計能力。
原文鏈接:??當我們談論DDD時我們在談論什么 (qq.com)??