作者:章磊(章三) 阿里飛豬技術團隊
一、為什么需要DDD
對于一個架構(gòu)師來說,在軟件開發(fā)中如何降低系統(tǒng)復雜度是一個永恒的挑戰(zhàn)。
- 復雜系統(tǒng)設計: 系統(tǒng)多,業(yè)務邏輯復雜,概念不清晰,有什么合適的方法幫助我們理清楚邊界,邏輯和概念
- 多團隊協(xié)同: 邊界不清晰,系統(tǒng)依賴復雜,語言不統(tǒng)一導致溝通和理解困難。有沒有一種方式把業(yè)務和技術概念統(tǒng)一,大家用一種語言溝通。例如:航程是大家所理解的航程嗎?
- 設計與實現(xiàn)一致性: PRD,詳細設計和代碼實現(xiàn)天差萬別。有什么方法可以把業(yè)務需求快速轉(zhuǎn)換為設計,同時還要保持設計與代碼的一致性?
- 架構(gòu)統(tǒng)一,可復用資產(chǎn)和擴展性: 當前取決于開發(fā)的同學具備很好的抽象能力和高編程的技能。有什么好的方法指導我們做抽象和實現(xiàn)。
二、DDD的價值
- 邊界清晰的設計方法: 通過領域劃分,識別哪些需求應該在哪些領域,不斷拉齊團隊對需求的認知,分而治之,控制規(guī)模。
- 統(tǒng)一語言: 團隊在有邊界的上下文中有意識地形成對事物進行統(tǒng)一的描述,形成統(tǒng)一的概念(模型)。
- 業(yè)務領域的知識沉淀: 通過反復論證和提煉模型,使得模型必須與業(yè)務的真實世界保持一致。促使知識(模型)可以很好地傳遞和維護。
- 面向業(yè)務建模: 領域模型與數(shù)據(jù)模型分離,業(yè)務復雜度和技術復雜度分離。
三、DDD架構(gòu)
3.1 分層架構(gòu)
- 用戶接口層: 調(diào)用應用層完成具體用戶請求。包含:controller,遠程調(diào)用服務等
- 應用層App: 盡量簡單,不包含業(yè)務規(guī)則,而只為了下一層中的領域對象做協(xié)調(diào)任務,分配工作,重點對領域?qū)幼鼍幣磐瓿蓮碗s業(yè)務場景。包含:AppService,消息處理等
- 領域?qū)覦omain: 負責表達業(yè)務概念和業(yè)務邏輯,領域?qū)邮窍到y(tǒng)的核心。包含:模型,值對象,域服務,事件
- 基礎層: 對所有商城提供技術能力,包括:數(shù)據(jù)操作,發(fā)送消息,消費消息,緩存等
- 調(diào)用關系: 用戶接口層->應用層->領域?qū)?>基礎層
- 依賴關系:用 戶接口層->應用層->領域?qū)?>基礎層
3.2 六邊形架構(gòu)
- 六邊形架構(gòu): 系統(tǒng)通過適配器的方式與外部交互,將應用服務于領域服務封裝在系統(tǒng)內(nèi)部
- 分層架構(gòu): 它依然是分層架構(gòu),它核心改變的是依賴關系。
- 領域?qū)右蕾嚨怪茫?領域?qū)右蕾嚮A層倒置成基礎層依賴領域?qū)?,這個簡單的變化使得領域?qū)硬灰蕾嚾蝿諏樱渌麑佣家蕾囶I域?qū)?,使得領域?qū)又槐磉_業(yè)務邏輯且穩(wěn)定。
3.3 調(diào)用鏈路
四、DDD的基本概念
4.1 領域模型
領域(戰(zhàn)略):業(yè)務范圍,范圍就是邊界。子領域:領域可大可小,我們將一個領域進行拆解形成子領域,子領域還可以進行拆解。當一個領域太大的時候需要進行細化拆解。模型(戰(zhàn)術):基于某個業(yè)務領域識別出這個業(yè)務領域的聚合,聚合根,界限上下文,實體,值對象。
4.1.1 核心域
決定產(chǎn)品和公司核心競爭力的子域是核心域,它是業(yè)務成功的主要因素和公司的核心競爭力。直接對業(yè)務產(chǎn)生價值。
4.1.2 通用域
沒有太多個性化的訴求,同時被多個子域使用的通用功能子域是通用域。例如,權限,登陸等等。間接對業(yè)務產(chǎn)生價值。
4.1.3 支撐域
支撐其他領域業(yè)務,具有企業(yè)特性,但不具有通用性。間接對業(yè)務產(chǎn)生價值。
4.1.4 為什么要劃分核心域、通用域和支撐域
一個業(yè)務一定有他最重要的部分,在日常做業(yè)務判斷和需求優(yōu)先級判斷的時候可以基于這個劃分來做決策。例如:一個交易相關的需求和一個配置相關的需求排優(yōu)先級,很明顯交易是核心域,規(guī)則是支持域。同樣我們認為是支撐域或者通用域的在其他公司可能是核心域,例如權限對于我們來說是通用域,但是對于專業(yè)做權限系統(tǒng)的公司,這個是核心域。
4.2 限界上下文(戰(zhàn)略)
業(yè)務的邊界的劃分,這個邊界可以是一個領域或者多個領域的集合。復雜業(yè)務需要多個域編排完成一個復雜業(yè)務流程。限界上下文可以作為微服務劃分的方法。其本質(zhì)還是高內(nèi)聚低耦合,只是限界上下文只是站在更高的層面來進行劃分。如何進行劃分,我的方法是一個界限上下文必須支持一個完整的業(yè)務流程,保證這個業(yè)務流程所涉及的領域都在一個限界上下文中。
4.3 實體(ENTITY)
定義: 實體有唯一的標識,有生命周期且具有延續(xù)性。例如一個交易訂單,從創(chuàng)建訂單我們會給他一個訂單編號并且是唯一的這就是實體唯一標識。同時訂單實體會從創(chuàng)建,支付,發(fā)貨等過程最終走到終態(tài)這就是實體的生命周期。訂單實體在這個過程中屬性發(fā)生了變化,但訂單還是那個訂單,不會因為屬性的變化而變化,這就是實體的延續(xù)性。實體的業(yè)務形態(tài): 實體能夠反映業(yè)務的真實形態(tài),實體是從用例提取出來的。領域模型中的實體是多個屬性、操作或行為的載體。實體的代碼形態(tài): 我們要保證實體代碼形態(tài)與業(yè)務形態(tài)的一致性。那么實體的代碼應該也有屬性和行為,也就是我們說的充血模型,但實際情況下我們使用的是貧血模型。貧血模型缺點是業(yè)務邏輯分散,更像數(shù)據(jù)庫模型,充血模型能夠反映業(yè)務,但過重依賴數(shù)據(jù)庫操作,而且復雜場景下需要編排領域服務,會導致事務過長,影響性能。所以我們使用充血模型,但行為里面只涉及業(yè)務邏輯的內(nèi)存操作。實體的運行形態(tài): 實體有唯一ID,當我們在流程中對實體屬性進行修改,但ID不會變,實體還是那個實體。實體的數(shù)據(jù)庫形態(tài): 實體在映射數(shù)據(jù)庫模型時,一般是一對一,也有一對多的情況。
4.4 值對象(VALUEOBJECT)
定義:通過對象屬性值來識別的對象,它將多個相關屬性組合為一個概念整體。在 DDD 中用來描述領域的特定方面,并且是一個沒有標識符的對象,叫作值對象。值對象沒有唯一標識,沒有生命周期,不可修改,當值對象發(fā)生改變時只能替換(例如String的實現(xiàn))值對象的業(yè)務形態(tài): 值對象是描述實體的特征,大多數(shù)情況一個實體有很多屬性,一般都是平鋪,這些數(shù)據(jù)進行分類和聚合后能夠表達一個業(yè)務含義,方便溝通而不關注細節(jié)。值對象的代碼形態(tài): 實體的單一屬性是值對象,例如:字符串,整型,枚舉。多個屬性的集合也是值對象,這個時候我們把這個集合設計為一個CLASS,但沒有ID。例如商品實體下的航段就是一個值對象。航段是描述商品的特征,航段不需要ID,可以直接整體替換。商品為什么是一個實體,而不是描述訂單特征,因為需要表達誰買了什么商品,所以我們需要知道哪一個商品,因此需要ID來標識唯一性。我們看一下下面這段代碼,person 這個實體有若干個單一屬性的值對象,比如 Id、name 等屬性;同時它也包含多個屬性的值對象,比如地址 address。
值對象的運行形態(tài): 值對象創(chuàng)建后就不允許修改了,只能用另外一個值對象來整體替換。當我們修改地址時,從頁面?zhèn)魅胍粋€新的地址對象替換調(diào)用person對象的地址即可。如果我們把address設計成實體,必然存在ID,那么我們需要從頁面?zhèn)魅氲牡刂穼ο蟮腎D與person里面的地址對像的ID進行比較,如果相同就更新,如果不同先刪除數(shù)據(jù)庫在新增數(shù)據(jù)。值對象的數(shù)據(jù)庫形態(tài): 有兩種方式嵌入式和序列化大對象。案例1:以屬性嵌入的方式形成的人員實體對象,地址值對象直接以屬性值嵌入人員實體中。
當我們只有一個地址的時候使用嵌入式比較好,如果多個地址必須有序列化大對象。同時可以支持搜索。
案例2:以序列化大對象的方式形成的人員實體對象,地址值對象被序列化成大對象 Json 串后,嵌入人員實體中。
支持多個地址存儲,不支持搜索。
值對象的優(yōu)勢和局限:1.簡化數(shù)據(jù)庫設計,提升數(shù)據(jù)庫操作的性能(多表新增和修改,關聯(lián)表查詢)2.雖然簡化數(shù)據(jù)庫設計,但是領域模型還是可以表達業(yè)務3.序列化的方式會使搜索實現(xiàn)困難(通過搜索引擎可以解決)
4.5 聚合和聚合根
多個實體和值對象組成的我們叫聚合,聚合的內(nèi)部一定的高內(nèi)聚。這個聚合里面一定有一個實體是聚合根。聚合與領域的關系:聚合也是范圍的劃分,領域也是范圍的劃分。領域與聚合可以是一對一,也可以是一對多的關系聚合根的作用是保證內(nèi)部的實體的一致性,對外只需要對聚合根進行操作。
4.6 限界上下文,域,聚合,實體,值對象的關系
領域包含限界上下文,限界上下文包含子域,子域包含聚合,聚合包含實體和值對象
4.7 事件風暴
參與者
除了領域?qū)<?,事件風暴的其他參與者可以是DDD專家、架構(gòu)師、產(chǎn)品經(jīng)理、項目經(jīng)理、開發(fā)人員和測試人員等項目團隊成員
事件風暴準備的材料
一面墻和一支筆。
事件風暴的關注點
在領域建模的過程中,我們需要重點關注這類業(yè)務的語言和行為。比如某些業(yè)務動作或行為(事件)是否會觸發(fā)下一個業(yè)務動作,這個動作(事件)的輸入和輸出是什么?是誰(實體)發(fā)出的什么動作(命令),觸發(fā)了這個動作(事件)…我們可以從這些暗藏的詞匯中,分析出領域模型中的事件、命令和實體等領域?qū)ο蟆?/p>
實體執(zhí)行命令產(chǎn)生事件。
業(yè)務場景的分析
通過業(yè)務場景和用例找出實體,命令,事件。
領域建模
領域建模時,我們會根據(jù)場景分析過程中產(chǎn)生的領域?qū)ο?,比如命令、事件等之間關系,找出產(chǎn)生命令的實體,分析實體之間的依賴關系組成聚合,為聚合劃定限界上下文,建立領域模型以及模型之間的依賴。領域模型利用限界上下文向上可以指導微服務設計,通過聚合向下可以指導聚合根、實體和值對象的設計。
五、如何建模
- 用例場景梳理:就是一句話需求,但我們需要把一些模糊的概念通過對話的方式逐步得到明確的需求,在加以提煉和抽象。
- 建模方法論:詞法分析(找名詞和動詞),領域邊界
- 模型驗證
5.1 協(xié)同單自動化分單案例
5.1.1 領域建模
需求:我們需要把系統(tǒng)自動化失敗轉(zhuǎn)人工訂單自動分配給小二,避免人工挑單和搶單,通過自動分配提升整體履約處理效率。
- 產(chǎn)品小A:把需求讀了一遍…….。
- 開發(fā)小B:那就是將履約單分配給個小二對吧?
- 產(chǎn)品小A:不對,我們還需要根據(jù)一個規(guī)則自動分單,例如退票訂單分給退票的小二
- 開發(fā)小B:恩,那我們可以做一個分單規(guī)則管理。例如:新增一個退票分單規(guī)則,在里面添加一批小二工號。履約單基于自身屬性去匹配分單規(guī)則并找到一個規(guī)則,然后從分單規(guī)則里面選擇一個小二工號,履約單寫入小二工號即可。
- 產(chǎn)品小A:分單規(guī)則還需要有優(yōu)先級,其中小二如果上班了才分配,如果下班了就不分配。
- 開發(fā)小B:優(yōu)先級沒有問題,在匹配分單規(guī)則方法里面按照優(yōu)先級排序即可,不影響模型。而小二就不是簡單一個工號維護在分單規(guī)則中,小二有狀態(tài)了。
- 產(chǎn)品小A:分單規(guī)則里面添加小二操作太麻煩了,例如:每次新增一個規(guī)則都要去挑人,人也不一定記得住,實際客服在管理小二的時候是按照技能組管理的。
- 開發(fā)小B:恩,懂了,那就是通過新增一個技能組管理模塊來管理小二。然后在通過分單規(guī)則來配置1個技能組即可。獲取一個小二工號就在技能組里面了。
- 開發(fā)小B:總感覺不對,因為新增一個自動化分單需求,履約單就依賴了分單規(guī)則,履約單應該是一個獨立的域,分單不是履約的能力,履約單實際只需要知道處理人是誰,至于怎么分配的他不太關心。應該由分單規(guī)則基于履約單屬性找匹配一個規(guī)則,然后基于這個規(guī)則找到一個小二。履約單與分單邏輯解耦。
- 產(chǎn)品小A:分單要輪流分配或者能者多勞分配,小二之前處理過的訂單和航司優(yōu)先分配。
- 開發(fā)小B:獲取小二的邏輯越來越復雜了,實際技能組才是找小二的核心,分單規(guī)則核心是通過履約單特征得到一個規(guī)則結(jié)果(技能組ID,分單策略,特征規(guī)則)。技能組基于分單規(guī)則的結(jié)果獲得小二工號。
- 產(chǎn)品小A:還漏了一個信息,就是履約單會被多次分配的情況,每一個履約環(huán)節(jié)都可能轉(zhuǎn)人工,客服需要知道履約單被處理多次的情況
- 開發(fā)小B:那用履約單無法表達了,我們需要新增一個概念叫協(xié)同單,協(xié)同單是為了協(xié)同履約單,通過協(xié)同推進履約單的進度。
- 產(chǎn)品小A:協(xié)同單概念很好,小二下班后,如果沒有處理完,還可以轉(zhuǎn)交給別人。
- 開發(fā)小B:恩,那只需要在協(xié)同單上增加行為即可
5.1.2 領域劃分
溝通的過程就是推導和驗證模型的過程,最后進行域的劃分:
5.1.3 場景梳理
窮舉所有場景,重新驗證模型是否可以覆蓋所有場景。
場景名稱 | 鎖 | 場景動作 | 域 | 域服務 | 事件 | 聚合根 | 方法 |
創(chuàng)建協(xié)同單 | 無 | 1、判斷關聯(lián)業(yè)務單是否非法 | 協(xié)同單 | 創(chuàng)建協(xié)同單 1、問題分類是否符合條件 (例如:商家用戶發(fā)起自營->商家的協(xié)同單) 2、save | 協(xié)同單 | 創(chuàng)建協(xié)同單 | |
分配協(xié)同單 | 協(xié)同單ID | 分配協(xié)同單到人. 1、判斷協(xié)同單狀態(tài)(=待處理) 2、記錄操作日志 3、save | 協(xié)同單 | 分配協(xié)同單 | 協(xié)同單 | 分配協(xié)同單 | |
受理協(xié)同單 | 協(xié)同單ID | 處理協(xié)同單 | 協(xié)同單 | 受理協(xié)同單 1.判斷訂單狀態(tài)(=待處理/驗收失敗) 2.更改訂單狀態(tài)(待處理/驗收失敗->處理中) 3.記錄操作日志 4.save | 協(xié)同單 | 受理協(xié)同單 | |
轉(zhuǎn)交協(xié)同單 | 協(xié)同單ID | 轉(zhuǎn)交協(xié)同單 | 協(xié)同單 | 轉(zhuǎn)交協(xié)同單 1.判斷訂單狀態(tài).(=處理中、待處理) 2.更改協(xié)同人值對象(同一組織下的不同人,從坐席管理域中?。?/p> 3.記錄操作日志 4.save | 協(xié)同單 | 轉(zhuǎn)交協(xié)同單 | |
關閉協(xié)同單 | 協(xié)同單ID | 關閉協(xié)同單 | 協(xié)同單 | 關閉協(xié)同單 1.判斷訂單狀態(tài) (=處理中、待處理) 2.更改訂單狀態(tài) (關閉) 3.記錄操作日志 4.save | 協(xié)同單 | 關閉協(xié)同單 | |
處理協(xié)同單 | 協(xié)同單ID | 處理協(xié)同單 | 協(xié)同單 | 處理協(xié)同單 1.判斷訂單狀態(tài) (=處理中) 2.更改訂單狀態(tài)(處理中->待驗收) 3.記錄操作日志 4.save | 協(xié)同單 | 處理協(xié)同單 | |
駁回協(xié)同單 | 協(xié)同單ID | 駁回協(xié)同單 | 協(xié)同單 | 駁回協(xié)同單 1.判斷訂單狀態(tài) (=待驗收) 2.更改訂單狀態(tài)(待驗收->處理中) 3.記錄操作日志 4.save | 協(xié)同單 | 駁回協(xié)同單 | |
完結(jié)協(xié)同單 | 協(xié)同單ID | 完結(jié)協(xié)同單 | 協(xié)同單 | 完結(jié)協(xié)同單 1.判斷訂單狀態(tài) (=待驗收) 2.更改訂單狀態(tài)(待驗收->已完結(jié)) 3.記錄操作日志 4.save | 協(xié)同單 | 完結(jié)協(xié)同單 | |
拒絕協(xié)同單 | 協(xié)同單ID | 拒絕協(xié)同單 | 協(xié)同單 | 拒絕協(xié)同單 1.判斷訂單狀態(tài)(=處理中、待處理) 2.更改訂單狀態(tài)(已拒絕) 3.記錄操作日志 4.save | 協(xié)同單 | 拒絕協(xié)同單 | |
催單 | 協(xié)同單ID | 催單 | 協(xié)同單 | 催單 1.判斷訂單狀態(tài)(=處理中、待處理) 2、修改催單值對象 3、記錄操作日志 4、save | 協(xié)同單 | 催單 |
六、怎么寫代碼
6.1 DDD規(guī)范
每一層都定義了相應的接口主要目的是規(guī)范代碼:
application:CRQS模式,ApplicationCmdService是command,ApplicationQueryService是query
service:是領域服務規(guī)范,其中定義了DomainService,應用系統(tǒng)需要繼承它。
model:是聚合根,實體,值對象的規(guī)范。
- Aggregate和BaseAggregate:聚合根定義
- Entity和BaseEntity:實體定義
- Value和BaseValue:值對象定義
- Param和BaseParam:領域?qū)訁?shù)定義,用作域服務,聚合根和實體的方法參數(shù)
- Lazy:描述聚合根屬性是延遲加載屬性,類似與hibernate。
- Field:實體屬性,用來實現(xiàn)update-tracing
/** * 實體屬性,update-tracing * @param */public final class Field implements Changeable { private boolean changed = false; private T value; private Field(T value){ this.value = value; } public void setValue(T value){ if(!equalsValue(value)){ this.changed = true; } this.value = value; } @Override public boolean isChanged() { return changed; } public T getValue() { return value; } public boolean equalsValue(T value){ if(this.value == null && value == null){ return true; } if(this.value == null){ return false; } if(value == null){ return false; } return this.value.equals(value); } public static Field build(T value){ return new Field(value); }}
repository
- Repository:倉庫定義
- AggregateRepository:聚合根倉庫,定義聚合根常用的存儲和查詢方法
event:事件處理
exception:定義了不同層用的異常
- AggregateException:聚合根里面拋的異常
- RepositoryException:基礎層拋的異常
- EventProcessException:事件處理拋的
6.2 工程結(jié)構(gòu)
6.2.1 application模塊
- CRQS模式:commad和query分離。
- 重點做跨域的編排工作,無業(yè)務邏輯
6.2.2 domain模塊
域服務,聚合根,值對象,領域參數(shù),倉庫定義
6.2.3 infrastructurre模塊
所有技術代碼在這一層。mybatis,redis,mq,job,opensearch代碼都在這里實現(xiàn),domain通過依賴倒置不依賴這些技術代碼和JAR。
6.2.4 client模塊
對外提供服務
6.2.5 model模塊
內(nèi)外都要用的共享對象
6.3 代碼示例
6.3.1 application示例
public interface CaseAppFacade extends ApplicationCmdService { /** * 接手協(xié)同單 * @param handleCaseDto * @return */ ResultDO handle(HandleCaseDto handleCaseDto);}public class CaseAppImpl implements CaseAppFacade { @Resource private CaseService caseService;//域服務 @Resource CaseAssembler caseAssembler;//DTO轉(zhuǎn)Param @Override public ResultDO handle(HandleCaseDto handleCaseDto) { try { ResultDO resultDO = caseService.handle(caseAssembler.from(handleCaseDto)); if (resultDO.isSuccess()) { pushMsg(handleCaseDto.getId()); return ResultDO.buildSuccessResult(null); } return ResultDO.buildFailResult(resultDO.getMsg()); } catch (Exception e) { return ResultDO.buildFailResult(e.getMessage()); } }}
- mapstruct:VO,DTO,PARAM,DO,PO轉(zhuǎn)換非常方便,代碼量大大減少。
- CaseAppImpl.handle調(diào)用域服務caseService.handle
6.3.2 domainService示例
public interface CaseService extends DomainService { /** * 接手協(xié)同單 * * @param handleParam * @return */ ResultDO handle(HandleParam handleParam); }public class CaseServiceImpl implements CaseService { @Resourceprivate CoordinationRepository coordinationRepository; @Override public ResultDO handle(HandleParam handleParam) { SyncLock lock = null; try { lock = coordinationRepository.syncLock(handleParam.getId().toString()); if (null == lock) { return ResultDO.buildFailResult(“協(xié)同單handle加鎖失敗”); } CaseAggregate caseAggregate = coordinationRepository.query(handleParam.getId()); caseAggregate.handle(handleParam.getFollowerValue()); coordinationRepository.save(caseAggregate); return ResultDO.buildSuccessResult(null); } catch (RepositoryException | AggregateException e) { String msg = LOG.error4Tracer(OpLogConstant.traceId(handleParam.getId()), e, “協(xié)同單handle異常”); return ResultDO.buildFailResult(msg); } finally { if (null != lock) { coordinationRepository.unlock(lock); } } }}
- 領域?qū)硬灰蕾嚮A層的實現(xiàn): coordinationRepository只是接口,在領域?qū)佣x好,由基礎層依賴領域?qū)訉崿F(xiàn)這個接口
- 業(yè)務邏輯和技術解耦: 域服務這層通過調(diào)用coordinationRepository和聚合根將業(yè)務邏輯和技術解耦。
- 聚合根的方法無副作用: 聚合根的方法只對聚合根內(nèi)部實體屬性的改變,不做持久化動作,可反復測試。
- 模型與數(shù)據(jù)分離: 改變模型:caseAggregate.handle(handleParam.getFollowerValue()); 改變數(shù)據(jù):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; } /** * 接手協(xié)同單 * @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 { /** * 創(chuàng)建時間 */ private Field gmtCreate; /** * 修改時間 */ private Field gmtModified; /** * 問題分類 */ private Field caseType; /** * 是否需要支付 */ private Field needPayFlag; /** * 是否需要自動驗收通過協(xié)同單 */ private Field autoAcceptCoordinationFlag; /** * 發(fā)起協(xié)同人值對象 */ private Field creatorValue; /** * 跟進人 */ private Field followerValue; /** * 狀態(tài) */ private Field status; /** * 關聯(lián)協(xié)同單id */ private Field relatedCaseId; /** * 關聯(lián)協(xié)同單類型 * @see 讀配置 com.alitrip.agent.business.flight.common.model.dataobject.CoordinationCaseTypeDO */ private Field relatedBizType; /** * 支付狀態(tài) */ private Field payStatus; 省略…. public CaseFeatureValue getCaseFeatureValue() { return get(caseFeatureValue); } public Boolean isCaseFeatureValueChanged() { return caseFeatureValue.isChanged(); } public void setCaseFeatureValue(CaseFeatureValue caseFeatureValue) { this.caseFeatureValue = set(this.caseFeatureValue, caseFeatureValue); } public Boolean isPayStatusChanged() { return payStatus.isChanged(); } public Boolean isGmtCreateChanged() { return gmtCreate.isChanged(); } public Boolean isGmtModifiedChanged() { return gmtModified.isChanged(); } public Boolean isCaseTypeChanged() { return caseType.isChanged(); } 省略…. /** * 接手 */ public void handle(FollowerValue followerValue) throws AggregateException { if (isWaitProcess()||isAppointProcess()) { this.setFollowerValue(followerValue); this.setStatus(CaseStatusEnum.PROCESSING); this.setGmtModified(new Date()); initCaseRecordValue(CaseActionNameEnum.HANDLE, null, followerValue); } else { throwStatusAggregateException(); } } 省略….}
充血模型VS貧血模型:
- 充血模型:表達能力強,代碼高內(nèi)聚,領域內(nèi)封閉,聚合根內(nèi)部結(jié)構(gòu)對外不可見,通過聚合根的方法訪問,適合復雜企業(yè)業(yè)務邏輯。
- 貧血模型:業(yè)務復雜之后,邏輯散落到大量方法中。
規(guī)范大于技巧:DDD架構(gòu)可以避免引入一些其他概念,系統(tǒng)只有域,域服務,聚合根,實體,值對象,事件來構(gòu)建系統(tǒng)。
聚合根的reconProcess的方法的業(yè)務邏輯被reconHandler和reconRiskHandler處理,必然這些handler要訪問聚合根里面的實體的屬性,那么邏輯就會散落。修改后:
沒有引入其他概念,都是在聚合根里面組織實體完成具體業(yè)務邏輯,去掉了handler這種技術語言。
- 聚合根和實體定義的方法是具備單一原則,復用性原則與使用場景無關,例如:不能定義手工創(chuàng)建協(xié)調(diào)單和系統(tǒng)自動創(chuàng)建協(xié)同單,應該定義創(chuàng)建協(xié)同單。
- Update-tracing: handle方法修改屬性后,然后調(diào)用 coordinationRepository.save(caseAggregate),我們只能全量屬性更新。Update-tracing是監(jiān)控實體的變更。 Entiy定義屬性通過Field進行包裝實現(xiàn)屬性的變更狀態(tài)記錄,結(jié)合mapstruct轉(zhuǎn)換PO實現(xiàn)Update-tracing。
修改了mapstruct生成轉(zhuǎn)換代碼的源碼,修改后生成的代碼:
當屬性被改變后就轉(zhuǎn)換到po中,這樣就可以實現(xiàn)修改后的字段更新。修改后的mapstruct代碼地址:[email protected]: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 { //聚合根轉(zhuǎn)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); } // 發(fā)送協(xié)同單狀態(tài)改變消息 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接口定義在領域?qū)?/li>
- CoordinationRepositoryImpl實現(xiàn)在基礎層:數(shù)據(jù)庫操作都是基于聚合根操作,保證聚合根里面的實體強一致性。
七、最后結(jié)束語
- 好的模型,可以沉淀組織資產(chǎn),不好的模型,逐漸成為負債
- 功能才是表象,模型才是內(nèi)在
- 建模過程是不斷猜想與反駁的過程
- 演化觀點是建模過程的基本心智模式