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