作者:章磊(章三) 阿里飛豬技術團隊
一、為什麼需要DDD
對於一個架構師來說,在軟體開發中如何降低系統複雜度是一個永恆的挑戰。
複雜系統設計:
系統多,業務邏輯複雜,概念不清晰,有什麼合適的方法幫助我們理清楚邊界,邏輯和概念
多團隊協同:
邊界不清晰,系統依賴複雜,語言不統一導致溝通和理解困難。有沒有一種方式把業務和技術概念統一,大家用一種語言溝通。例如:航程是大家所理解的航程嗎?
設計與實現一致性:
PRD,詳細設計和程式碼實現天差萬別。有什麼方法可以把業務需求快速轉換為設計,同時還要保持設計與程式碼的一致性?
架構統一,可複用資產和擴充套件性:
當前取決於開發的同學具備很好的抽象能力和高程式設計的技能。有什麼好的方法指導我們做抽象和實現。
二、DDD的價值
邊界清晰的設計方法:
透過領域劃分,識別哪些需求應該在哪些領域,不斷拉齊團隊對需求的認知,
分而治之,控制規模
。
統一語言:
團隊在有邊界的上下文中有意識地形成對事物進行統一的描述,形成統一的
概念(模型)
。
業務領域的知識沉澱:
透過
反覆論證
和
提煉
模型,使得模型必須與業務的真實世界
保持一致
。促使知識(模型)可以很好地傳遞和維護。
面向業務建模:
領域模型與資料模型分離,
業務複雜度和技術複雜度分離
。
三、DDD架構
3。1 分層架構
使用者介面層:
呼叫應用層完成具體使用者請求。包含:controller,遠端呼叫服務等
應用層App:
儘量簡單,不包含業務規則,而只為了下一層中的領域物件做協調任務,分配工作,重點對領域層做編排完成複雜業務場景。包含:AppService,訊息處理等
領域層Domain:
負責表達業務概念和業務邏輯,領域層是系統的核心。包含:模型,值物件,域服務,事件
基礎層:
對所有商城提供技術能力,包括:資料操作,傳送訊息,消費訊息,快取等
呼叫關係:
使用者介面層->應用層->領域層->基礎層
依賴關係:用
戶介面層->應用層->領域層->基礎層
3。2 六邊形架構
六邊形架構:
系統透過介面卡的方式與外部互動,將應用服務於領域服務封裝在系統內部
分層架構:
它依然是分層架構,它核心改變的是依賴關係。
領域層依賴倒置:
領域層依賴基礎層倒置成基礎層依賴領域層,這個簡單的變化使得領域層不依賴任務層,其他層都依賴領域層,使得領域層只表達業務邏輯且穩定。
3。3 呼叫鏈路
四、DDD的基本概念
4。1 領域模型
領域(戰略):業務範圍,範圍就是邊界。
子領域:領域可大可小,我們將一個領域進行拆解形成子領域,子領域還可以進行拆解。當一個領域太大的時候需要進行細化拆解。
模型(戰術):基於某個業務領域識別出這個業務領域的聚合,聚合根,界限上下文,實體,值物件。
4。1。1 核心域
決定產品和公司核心競爭力的子域是核心域,它是業務成功的主要因素和公司的核心競爭力。
直接對業務產生價值
。
4。1。2 通用域
沒有太多個性化的訴求,同時被多個子域使用的通用功能子域是通用域。例如,許可權,登陸等等。
間接對業務產生價值
。
4。1。3 支撐域
支撐其他領域業務,具有企業特性,但不具有通用性。
間接對業務產生價值
。
4。1。4 為什麼要劃分核心域、通用域和支撐域
一個業務一定有他最重要的部分,在日常做業務判斷和需求優先順序判斷的時候可以基於這個劃分來做決策。例如:一個交易相關的需求和一個配置相關的需求排優先順序,很明顯交易是核心域,規則是支援域。同樣我們認為是支撐域或者通用域的在其他公司可能是核心域,例如許可權對於我們來說是通用域,但是對於專業做許可權系統的公司,這個是核心域。
4。2 限界上下文(戰略)
業務的邊界的劃分,這個邊界可以是一個領域或者多個領域的集合。複雜業務需要多個域編排完成一個複雜業務流程。限界上下文可以作為微服務劃分的方法。其本質還是高內聚低耦合,只是限界上下文只是站在更高的層面來進行劃分。如何進行劃分,我的方法是一個界限上下文必須支援一個完整的業務流程,保證這個業務流程所涉及的領域都在一個限界上下文中。
4。3 實體(ENTITY)
定義:
實體有唯一的標識,有生命週期且具有延續性。例如一個交易訂單,從建立訂單我們會給他一個訂單編號並且是唯一的這就是
實體唯一標識
。同時訂單實體會從建立,支付,發貨等過程最終走到終態這就是
實體的生命週期
。訂單實體在這個過程中屬性發生了變化,但訂單還是那個訂單,不會因為屬性的變化而變化,這就是
實體的延續性
。
實體的業務形態:
實體能夠反映業務的真實形態,實體是從用例提取出來的。領域模型中的實體是多個屬性、操作或行為的載體。
實體的程式碼形態:
我們要保證實體程式碼形態與業務形態的一致性。那麼實體的程式碼應該也有屬性和行為,也就是我們說的充血模型,但實際情況下我們使用的是貧血模型。貧血模型缺點是業務邏輯分散,更像資料庫模型,充血模型能夠反映業務,但過重依賴資料庫操作,而且複雜場景下需要編排領域服務,會導致事務過長,影響效能。所以我們使用充血模型,但行為裡面只涉及業務邏輯的記憶體操作。
實體的執行形態:
實體有唯一ID,當我們在流程中對實體屬性進行修改,但ID不會變,實體還是那個實體。
實體的資料庫形態:
實體在對映資料庫模型時,一般是一對一,也有一對多的情況。
4。4 值物件(VALUEOBJECT)
定義:透過物件屬性值來識別的物件,它將多個相關屬性組合為一個概念整體。在 DDD 中用來描述領域的特定方面,並且是一個沒有識別符號的物件,叫作值物件。值物件沒有唯一標識,沒有生命週期,不可修改,當值物件發生改變時只能替換(例如String的實現)
值物件的業務形態:
值物件是描述實體的特徵,大多數情況一個實體有很多屬性,一般都是平鋪,這些資料進行分類和聚合後能夠表達一個業務含義,方便溝通而不關注細節。
值物件的程式碼形態:
實體的單一屬性是值物件,例如:字串,整型,列舉。多個屬性的集合也是值物件,這個時候我們把這個集合設計為一個CLASS,但沒有ID。例如商品實體下的航段就是一個值物件。航段是描述商品的特徵,航段不需要ID,可以直接整體替換。商品為什麼是一個實體,而不是描述訂單特徵,因為需要表達誰買了什麼商品,所以我們需要知道哪一個商品,因此需要ID來標識唯一性。
我們看一下下面這段程式碼,person 這個實體有若干個單一屬性的值物件,比如 Id、name 等屬性;同時它也包含多個屬性的值物件,比如地址 address。
值物件的執行形態:
值物件建立後就不允許修改了,只能用另外一個值物件來整體替換。當我們修改地址時,從頁面傳入一個新的地址物件替換呼叫person物件的地址即可。如果我們把address設計成實體,必然存在ID,那麼我們需要從頁面傳入的地址物件的ID與person裡面的地址對像的ID進行比較,如果相同就更新,如果不同先刪除資料庫在新增資料。
值物件的資料庫形態:
有兩種方式嵌入式和序列化大物件。
案例1:以屬性嵌入的方式形成的人員實體物件,地址值物件直接以屬性值嵌入人員實體中。
當我們只有一個地址的時候使用嵌入式比較好,如果多個地址必須有序列化大物件。同時可以支援搜尋。
案例2:以序列化大物件的方式形成的人員實體物件,地址值物件被序列化成大物件 Json 串後,嵌入人員實體中。
支援多個地址儲存,不支援搜尋。
值物件的優勢和侷限:
1。簡化資料庫設計,提升資料庫操作的效能(多表新增和修改,關聯表查詢)
2。雖然簡化資料庫設計,但是領域模型還是可以表達業務
3。序列化的方式會使搜尋實現困難(透過搜尋引擎可以解決)
4。5 聚合和聚合根
多個實體和值物件組成的我們叫聚合,聚合的內部一定的高內聚。這個聚合裡面一定有一個實體是聚合根。
聚合與領域的關係:聚合也是範圍的劃分,領域也是範圍的劃分。領域與聚合可以是一對一,也可以是一對多的關係
聚合根的作用是保證內部的實體的一致性,對外只需要對聚合根進行操作。
4。6 限界上下文,域,聚合,實體,值物件的關係
領域包含限界上下文,限界上下文包含子域,子域包含聚合,聚合包含實體和值物件
4。7 事件風暴
參與者
除了領域專家,事件風暴的其他參與者可以是DDD專家、架構師、產品經理、專案經理、開發人員和測試人員等專案團隊成員
事件風暴準備的材料
一面牆和一支筆。
事件風暴的關注點
在領域建模的過程中,我們需要重點關注這類業務的語言和行為。比如某些業務動作或行為(事件)是否會觸發下一個業務動作,這個動作(事件)的輸入和輸出是什麼?是誰(實體)發出的什麼動作(命令),觸發了這個動作(事件)…我們可以從這些暗藏的詞彙中,分析出領域模型中的事件、命令和實體等領域物件。
實體執行命令產生事件。
業務場景的分析
透過業務場景和用例找出實體,命令,事件。
領域建模
領域建模時,我們會根據場景分析過程中產生的領域物件,比如命令、事件等之間關係,找出產生命令的實體,分析實體之間的依賴關係組成聚合,為聚合劃定限界上下文,建立領域模型以及模型之間的依賴。領域模型利用限界上下文向上可以指導微服務設計,透過聚合向下可以指導聚合根、實體和值物件的設計。
五、如何建模
用例場景梳理:就是一句話需求,但我們需要把一些模糊的概念透過對話的方式逐步得到明確的需求,在加以提煉和抽象。
建模方法論:詞法分析(找名詞和動詞),領域邊界
模型驗證
5。1 協同單自動化分單案例
5。1。1 領域建模
需求:我們需要把系統自動化失敗轉人工訂單自動分配給小二,避免人工挑單和搶單,透過自動分配提升整體履約處理效率。
產品小A:把需求讀了一遍……。。
開發小B:那就是將
履約單
分配給個
小二
對吧?
產品小A:不對,我們還需要根據一個規則自動分單,例如退票訂單分給退票的小二
開發小B:恩,那我們可以做一個
分單規則
管理。例如:新增一個退票分單規則,在裡面新增一批小二工號。履約單基於自身屬性去匹配分單規則並找到一個規則,然後從分單規則裡面選擇一個小二工號,履約單寫入小二工號即可。
產品小A:分單規則還需要有優先順序,其中小二如果上班了才分配,如果下班了就不分配。
開發小B:優先順序沒有問題,在匹配分單規則方法裡面按照優先順序排序即可,不影響模型。而小二就不是簡單一個工號維護在分單規則中,小二有狀態了。
產品小A:分單規則裡面新增小二操作太麻煩了,例如:每次新增一個規則都要去挑人,人也不一定記得住,實際客服在管理小二的時候是按照
技能組
管理的。
開發小B:恩,懂了,那就是透過新增一個技能組管理模組來管理小二。然後在透過分單規則來配置1個技能組即可。獲取一個小二工號就在技能組裡面了。
開發小B:總感覺不對,因為新增一個自動化分單需求,履約單就依賴了分單規則,履約單應該是一個獨立的域,分單不是履約的能力,履約單實際只需要知道處理人是誰,至於怎麼分配的他不太關心。應該由分單規則基於
履約單屬性
找匹配一個規則,然後基於這個規則找到一個小二。
履約單與分單邏輯解耦。
產品小A:分單要輪流分配或者能者多勞分配,小二之前處理過的訂單和航司優先分配。
開發小B:獲取小二的邏輯越來越複雜了,實際技能組才是找小二的核心,分單規則核心是透過履約單特徵得到一個規則結果(技能組ID,分單策略,特徵規則)。技能組基於分單規則的結果獲得小二工號。
產品小A:還漏了一個資訊,就是履約單會被多次分配的情況,每一個履約環節都可能轉人工,客服需要知道履約單被處理多次的情況
開發小B:那用履約單無法表達了,我們需要新增一個概念叫
協同單
,協同單是為了協同履約單,透過協同推進履約單的進度。
產品小A:協同單概念很好,小二下班後,如果沒有處理完,還可以轉交給別人。
開發小B:恩,那隻需要在協同單上增加行為即可
5。1。2 領域劃分
溝通的過程就是推導和驗證模型的過程,最後進行域的劃分:
5。1。3 場景梳理
窮舉所有場景,重新驗證模型是否可以覆蓋所有場景。
場景名稱
鎖
場景動作
域
域服務
事件
聚合根
方法
建立協同單
無
1、判斷關聯業務單是否非法
協同單
建立協同單
1、問題分類是否符合條件
(例如:商家使用者發起自營->商家的協同單)
2、save
協同單
建立協同單
分配協同單
協同單ID
分配協同單到人。
1、判斷協同單狀態(=待處理)
2、記錄操作日誌
3、save
協同單
分配協同單
協同單
分配協同單
受理協同單
協同單ID
處理協同單
協同單
受理協同單
1。判斷訂單狀態(=待處理/驗收失敗)
2。更改訂單狀態(待處理/驗收失敗->處理中)
3。記錄操作日誌
4。save
協同單
受理協同單
轉交協同單
協同單ID
轉交協同單
協同單
轉交協同單
1。判斷訂單狀態。(=處理中、待處理)
校驗轉交的人是否在正確的組織下面
2。更改協同人值物件(同一組織下的不同人,從坐席管理域中取)
3。記錄操作日誌
4。save
協同單
轉交協同單
關閉協同單
協同單ID
關閉協同單
協同單
關閉協同單
1。判斷訂單狀態
(=處理中、待處理)
2。更改訂單狀態
(關閉)
3。記錄操作日誌
4。save
協同單
關閉協同單
處理協同單
協同單ID
處理協同單
協同單
處理協同單
1。判斷訂單狀態
(=處理中)
2。更改訂單狀態(處理中->待驗收)
3。記錄操作日誌
4。save
協同單
處理協同單
駁回協同單
協同單ID
駁回協同單
協同單
駁回協同單
1。判斷訂單狀態
(=待驗收)
2。更改訂單狀態(待驗收->處理中)
3。記錄操作日誌
4。save
協同單
駁回協同單
完結協同單
協同單ID
完結協同單
協同單
完結協同單
1。判斷訂單狀態
(=待驗收)
2。更改訂單狀態(待驗收->已完結)
3。記錄操作日誌
4。save
協同單
完結協同單
拒絕協同單
協同單ID
拒絕協同單
協同單
拒絕協同單
1。判斷訂單狀態(=處理中、待處理)
2。更改訂單狀態(已拒絕)
3。記錄操作日誌
4。save
協同單
拒絕協同單
催單
協同單ID
催單
協同單
催單
1。判斷訂單狀態(=處理中、待處理)
2、修改催單值物件
3、記錄操作日誌
4、save
協同單
催單
六、怎麼寫程式碼
6。1 DDD規範
每一層都定義了相應的介面主要目的是規範程式碼:
application:CRQS模式,ApplicationCmdService是command,ApplicationQueryService是query
service:是領域服務規範,其中定義了DomainService,應用系統需要繼承它。
model:是聚合根,實體,值物件的規範。
Aggregate和BaseAggregate:聚合根定義
Entity和BaseEntity:實體定義
Value和BaseValue:值物件定義
Param和BaseParam:領域層引數定義,用作域服務,聚合根和實體的方法引數
Lazy:描述聚合根屬性是延遲載入屬性,類似與hibernate。
Field:實體屬性,用來實現update-tracing
/** * 實體屬性,update-tracing * @param
repository
Repository:倉庫定義
AggregateRepository:聚合根倉庫,定義聚合根常用的儲存和查詢方法
event:事件處理
exception:定義了不同層用的異常
AggregateException:聚合根裡面拋的異常
RepositoryException:基礎層拋的異常
EventProcessException:事件處理拋的
6。2 工程結構
6。2。1 application模組
CRQS模式:commad和query分離。
重點做跨域的編排工作,無業務邏輯
6。2。2 domain模組
域服務,聚合根,值物件,領域引數,倉庫定義
6。2。3 infrastructurre模組
所有技術程式碼在這一層。mybatis,redis,mq,job,opensearch程式碼都在這裡實現,domain透過依賴倒置不依賴這些技術程式碼和JAR。
6。2。4 client模組
對外提供服務
6。2。5 model模組
內外都要用的共享物件
6。3 程式碼示例
6。3。1 application示例
public interface CaseAppFacade extends ApplicationCmdService { /** * 接手協同單 * @param handleCaseDto * @return */ ResultDO
mapstruct:VO,DTO,PARAM,DO,PO轉換非常方便,程式碼量大大減少。
CaseAppImpl。handle呼叫域服務caseService。handle
6。3。2 domainService示例
public interface CaseService extends DomainService { /** * 接手協同單 * * @param handleParam * @return */ ResultDO
領域層不依賴基礎層的實現:
coordinationRepository只是介面,在領域層定義好,由基礎層依賴領域層實現這個介面
業務邏輯和技術解耦:
域服務這層透過呼叫coordinationRepository和聚合根將業務邏輯和技術解耦。
聚合根的方法無副作用:
聚合根的方法只對聚合根內部實體屬性的改變,不做持久化動作,可反覆測試。
模型與資料分離:
改變模型:caseAggregate。handle(handleParam。getFollowerValue()); 改變資料:coordinationRepository。save(caseAggregate);事務是在save方法上
6。3。3 Aggregate,Entity示例
public class CaseAggregate extends BaseAggregate implements NoticeMsgBuilder { private final CaseEntity caseEntity; public CaseAggregate(CaseEntity caseEntity) { this。caseEntity = caseEntity; } /** * 接手協同單 * @param followerValue * @return */ public void handle(FollowerValue followerValue) throws AggregateException { try { this。caseEntity。handle(followerValue); } catch (Exception e) { throw e; } }}public class CaseEntity extends BaseEntity { /** * 建立時間 */ private Field
充血模型VS貧血模型:
充血模型:表達能力強,程式碼高內聚,領域內封閉,聚合根內部結構對外不可見,透過聚合根的方法訪問,適合複雜企業業務邏輯。
貧血模型:業務複雜之後,邏輯散落到大量方法中。
規範大於技巧:DDD架構可以避免引入一些其他概念,系統只有域,域服務,聚合根,實體,值物件,事件來構建系統。
聚合根的reconProcess的方法的業務邏輯被reconHandler和reconRiskHandler處理,必然這些handler要訪問聚合根裡面的實體的屬性,那麼邏輯就會散落。修改後:
沒有引入其他概念,都是在聚合根裡面組織實體完成具體業務邏輯,去掉了handler這種技術語言。
聚合根和實體定義的方法是具備單一原則,複用性原則與使用場景無關
,例如:不能定義手工建立協調單和系統自動建立協同單,應該定義建立協同單。
Update-tracing:
handle方法修改屬性後,然後呼叫 coordinationRepository。save(caseAggregate),我們只能全量屬性更新。Update-tracing是監控實體的變更。 Entiy定義屬性透過Field進行包裝實現屬性的變更狀態記錄,結合mapstruct轉換PO實現Update-tracing。
修改了mapstruct生成轉換程式碼的原始碼,修改後生成的程式碼:
當屬性被改變後就轉換到po中,這樣就可以實現修改後的欄位更新。修改後的mapstruct程式碼地址:git@gitlab。alibaba-inc。com:flight-agent/mapstruct。git
idea的get和set方法自動生成:
由於使用field包裝,需要自定義get和set生成程式碼
6。3。4 Repository示例
public interface CoordinationRepository extends Repository { /** * 儲存/更新 * @param aggregate * @throws RepositoryException */ void save(CaseAggregate aggregate) throws RepositoryException;}@Repositorypublic class CoordinationRepositoryImpl implements CoordinationRepository { @Override public void save(CaseAggregate aggregate) throws RepositoryException { try { //聚合根轉PO,update-tracing技術 CasePO casePO = caseConverter。toCasePO(aggregate。getCase()); CasePO oldCasePO = null; if (aggregate。getCase()。isAppended()) { casePOMapper。insert(casePO); aggregate。getCase()。setId(casePO。getId()); } else { oldCasePO = casePOMapper。selectByPrimaryKey(casePO。getId()); casePOMapper。updateByPrimaryKeySelective(casePO); } // 傳送協同單狀態改變訊息 if (CaseStatusEnum。FINISH。getCode()。equals(casePO。getStatus()) || CaseStatusEnum。WAIT_DISTRIBUTION。getCode()。equals(casePO。getStatus()) || CaseStatusEnum。PROCESSING。getCode()。equals(casePO。getStatus()) || CaseStatusEnum。APPOINT_PROCESS。getCode()。equals(casePO。getStatus()) || CaseStatusEnum。WAIT_PROCESS。getCode()。equals(casePO。getStatus()) || CaseStatusEnum。CLOSE。getCode()。equals(casePO。getStatus()) || CaseStatusEnum。REJECT。getCode()。equals(casePO。getStatus()) || CaseStatusEnum。PENDING_ACCEPTANCE。getCode()。equals(casePO。getStatus())) { FollowerDto followerDto = new FollowerDto(); followerDto。setCurrentFollowerId(aggregate。getCase()。getFollowerValue()。getCurrentFollowerId()); followerDto。setCurrentFollowerGroupId(aggregate。getCase()。getFollowerValue()。getCurrentFollowerGroupId()); followerDto。setCurrentFollowerType(aggregate。getCase()。getFollowerValue()。getCurrentFollowerType()); followerDto。setCurrentFollowerName(aggregate。getCase()。getFollowerValue()。getCurrentFollowerName()); //拒絕和關閉都使用CLOSE String tag = CaseStatusEnum。codeOf(casePO。getStatus())。name(); if(CaseStatusEnum。REJECT。name()。equals(tag)){ tag = CaseStatusEnum。CLOSE。name(); } statusChangeProducer。send(CaseStatusChangeEvent。build() 。setId(casePO。getId()) 。setFollowerDto(followerDto) 。setStatus(aggregate。getCase()。getStatus()。getCode()) 。setCaseType(aggregate。getCase()。getCaseType()) 。setOldStatus(null != oldCasePO ? oldCasePO。getStatus() : null) 。setAppointTime(aggregate。getCase()。getAppointTime()), (tag)); } // 操作日誌 if (CollectionUtils。isNotEmpty(aggregate。getCase()。getCaseRecordValue())) { CaseRecordValue caseRecordValue = Lists。newArrayList(aggregate。getCase()。getCaseRecordValue())。get(0); caseRecordValue。setCaseId(casePO。getId()); recordPOMapper。insert(caseConverter。from(caseRecordValue)); } } catch (Exception e) { throw new RepositoryException(“”, e。getMessage(), e); } }}
CoordinationRepository介面定義在領域層
CoordinationRepositoryImpl實現在基礎層:資料庫操作都是基於聚合根操作,保證聚合根裡面的實體強一致性。
七、最後結束語
好的模型,可以沉澱組織資產,不好的模型,逐漸成為負債
功能才是表象,模型才是內在
建模過程是不斷猜想與反駁的過程
演化觀點是建模過程的基本心智模式