領(lǐng)域驅(qū)動設(shè)計(jì) DDD 實(shí)踐


背景

DDD 領(lǐng)域驅(qū)動設(shè)計(jì),想必大家都已經(jīng)耳熟能詳了,經(jīng)常能聽到『事件風(fēng)暴』、『聚合根』、『限界上下文』等等名詞,對其概念一知半解,又或者知道一些概念,又不知道如何落地實(shí)踐,怎么將設(shè)計(jì)轉(zhuǎn)換成代碼實(shí)現(xiàn),這篇文章或許可以幫到你。

ps: 有些遺漏的概念直接看末尾的參考目錄的文章就好了,已經(jīng)寫的很詳細(xì)了,這里就不再贅述,本文著重于實(shí)踐方式的說明。

DDD干的事兒:提供一套方法/工具、標(biāo)準(zhǔn)套路,對復(fù)雜領(lǐng)域進(jìn)行分析建模,讓參與設(shè)計(jì)的人,不論是研發(fā)還是客戶都能達(dá)成認(rèn)知的一致。

DDD不干的事兒DDD不能降低系統(tǒng)的復(fù)雜度,也不能幫你減少編碼,還會增加初期的設(shè)計(jì)開發(fā)時間

DDD不是銀彈,它更適用于成熟穩(wěn)定的業(yè)務(wù)線,適用于一些具有業(yè)務(wù)不變性的領(lǐng)域。很多面向C端客戶時時刻刻變化的需求就不太適合DDD;公司早期也不適合使用DDD,會導(dǎo)致開發(fā)復(fù)雜度開發(fā)時間大幅增加(業(yè)務(wù)一變,可能導(dǎo)致從底層開始每一層都需要推倒重來),對公司一些逐漸穩(wěn)定的業(yè)務(wù)可以嘗試使用DDD進(jìn)行重構(gòu)(穩(wěn)定的東西大概也沒人去碰了吧


實(shí)踐

DDD的名詞概念不清楚?別人家的DDD領(lǐng)域圖畫的很炫酷?不用慌,跟著我一起走,我們擼代碼畫圖都是一把梭。

大部分老鐵都應(yīng)該接觸過CRM系統(tǒng),接下來我將以CRM系統(tǒng)中的部分內(nèi)容進(jìn)行距離講解。

  • 畫圖工具

所以示例均基于該工具繪圖:miro.com

媽媽再也不用擔(dān)心我畫不好圖了!

image.png

事件風(fēng)暴

首先抓幾位幸運(yùn)的研發(fā)小伙伴和產(chǎn)品(通常作為研發(fā)沒辦法直面一線客戶,也沒有相應(yīng)的領(lǐng)域?qū)<?,所以產(chǎn)品就是最了解業(yè)務(wù)本身的人了),一起來開始事件風(fēng)暴吧。

啪!

先放一張事件風(fēng)暴的基礎(chǔ)元素在這里,每個顏色的貼紙代表什么意思現(xiàn)在先不用關(guān)心,后面回來查看就行了。

image.png
  • 事件梳理

CRM系統(tǒng)(全稱客戶關(guān)系管理系統(tǒng)),通常包含從:商機(jī)獲客 -> 線索 -> 銷售機(jī)會 -> 客戶 -> 客戶公海 等等功能模塊,大同小異。這里就以銷售機(jī)會進(jìn)行舉例。

首先和產(chǎn)品同學(xué)一起梳理出『銷售機(jī)會』業(yè)務(wù)中發(fā)生的所有事件,事件使用橙色標(biāo)簽,使用動詞的過去式進(jìn)行描述,比如:銷售機(jī)會已創(chuàng)建

image.png

有異議的地方可以標(biāo)記為熱點(diǎn)問題(超過5分鐘未達(dá)成一致),后續(xù)討論,不影響事件風(fēng)暴主流程:

image.png

ps:

  1. 區(qū)分事實(shí)和方案:DDD分析建議以現(xiàn)實(shí)中真實(shí)發(fā)生的事件為主,系統(tǒng)已有的功能多是軟件設(shè)計(jì)中的取舍,并非現(xiàn)實(shí)生活中發(fā)生的事實(shí),只是一個實(shí)現(xiàn)方案而已,事件風(fēng)暴分析過程中盡量以還原業(yè)務(wù)本身為主(比如銷售機(jī)會導(dǎo)出,只是一個方案而非事件)。
  2. 狀態(tài)已變更:這個階段很容易錯誤把狀態(tài)變更當(dāng)做事件,狀態(tài)變更是一個典型的方案而非事件,事件觸發(fā)最終導(dǎo)致了狀態(tài)的變更。
  3. 多個業(yè)務(wù)同時事件風(fēng)暴:可以按業(yè)務(wù)組劃分,一組小伙伴一起畫一份,最后一起分析,方便了解其他小伙伴負(fù)責(zé)的業(yè)務(wù)。

接下來你有半小時和小伙伴們梳理出所有事件(不用管對錯都先放出來),按事件發(fā)生的順序,從左向右,從上到下進(jìn)行排列。

image.png
  • 移除不屬于當(dāng)前業(yè)務(wù)的事件

這里我們認(rèn)為銷售匯報(bào)不屬于銷售機(jī)會業(yè)務(wù)的事件,我們畫一條豎線,將所有人達(dá)成一致的放右邊,不屬于當(dāng)前業(yè)務(wù)的事件放左邊。

image.png

命令風(fēng)暴

事件都梳理好了,那事件總得有人觸發(fā)吧,事件的觸發(fā)可能還得依賴某些規(guī)則,外部系統(tǒng)等等。這就是命令風(fēng)暴:梳理清楚業(yè)務(wù)中的事件是如何發(fā)生的,搞清楚這些事件的因果關(guān)系。

命令風(fēng)暴階段主要涉及到以下標(biāo)簽:

image.png
  • 事件的觸發(fā)和依賴關(guān)系

接下來我們給事件加一點(diǎn)點(diǎn)細(xì)節(jié)?。?0分鐘過去了)

ps:

  1. 事件觸發(fā):通常為內(nèi)部用戶或外部系統(tǒng)
  2. 策略:如果是做整體戰(zhàn)略設(shè)計(jì),這個階段就不用將依賴的規(guī)則列的很細(xì)致;如果是做業(yè)務(wù)設(shè)計(jì),建議列的盡可能詳盡,后續(xù)編碼可以直接作為參考。
image.png
  • 事件的因果關(guān)聯(lián)

這一步比較簡單,找到事件的上下游關(guān)系,依賴的外部系統(tǒng),關(guān)聯(lián)的策略都可以給連起來了。這個階段需要研發(fā)和產(chǎn)品同學(xué)對事件觸發(fā)的原因等達(dá)成一致。(又30分鐘過去了,battle不過產(chǎn)品先在自己身上找原因.jpg)

ps:

  1. 虛線:事件并非100%觸發(fā),允許異步觸發(fā)或者需要根據(jù)策略進(jìn)行判斷。
  2. 實(shí)現(xiàn):強(qiáng)一致性,上游的事件觸發(fā),必定會觸發(fā)下游事件。
image.png

尋找聚合

尋找聚合,主要是找到業(yè)務(wù)的邊界,建立統(tǒng)一的業(yè)務(wù)模型。

  • 建立聚合
image.png

操作步驟:

  1. 我們先需要先貼一個銷售機(jī)會的聚合。
  2. 然后將相關(guān)的貼紙拖到聚合旁邊,圍城一圈按貼紙顏色進(jìn)行擺放即可,沒有固定順序。
  3. 相同的貼紙可以只保留一個了,比如:事件的觸發(fā)角色只用保留一個員工。
  4. 已有的關(guān)聯(lián)連線保留不要刪除。

ps:

  1. 邊界劃分:比如銷售機(jī)會階段的變更都?xì)w屬于銷售機(jī)會的整個生命周期內(nèi),可以劃分到銷售機(jī)會內(nèi)。而關(guān)聯(lián)訂單的變更,來源于外部系統(tǒng),可能銷售機(jī)會已經(jīng)完成了還會有訂單的綁定或者解綁到銷售機(jī)會,最好劃分到外部。
  2. 聚合的大小:一個聚合通常對應(yīng)的我們的一個業(yè)務(wù)實(shí)體,聚合不要太大,也不要太小。太大的聚合通常是沒有梳理清楚聚合的邊界,可以繼續(xù)拆分;太小的聚合單獨(dú)維護(hù)麻煩。
image.png

子域劃分

子域的劃分主要是找到核心域、支撐域、公共域。子域的劃分沒有標(biāo)準(zhǔn)答案,這一步建議每個人單獨(dú)做,然后輪流分享自己的想法,最終達(dá)成一致即可。

  1. 核心域:領(lǐng)域最主要解決的問題,需要投入最優(yōu)先的人力物力,直接決定了產(chǎn)品的競爭力。如:對銷售機(jī)會階段的管理。
  2. 支持域:非核心領(lǐng)域,但又不可或缺,決定了用戶體驗(yàn)的好壞。如:銷售機(jī)會分類的管理,擁有多套銷售機(jī)會的切換,對大體量客戶使用體驗(yàn)更佳,無需多個業(yè)務(wù)組混用一套銷售機(jī)會。
  3. 公共域:提供一些通用能力,通常和業(yè)務(wù)無強(qiáng)關(guān)聯(lián)。如:用戶登錄、消息通知等
image.png

劃分上下文

終于到領(lǐng)域設(shè)計(jì)的最后一步了(前面已經(jīng)和產(chǎn)品同學(xué)battle了2小時,不要慌最后半小時了,battle完就可以去干飯了)

其實(shí)到上一部領(lǐng)域劃分完成,DDD的主體設(shè)計(jì)就算完成了。劃分限界上下文,更多的是從系統(tǒng)架構(gòu)層面,確定業(yè)務(wù)的解決方案,最常見的就是微服務(wù)架構(gòu),天生可以一個服務(wù)對應(yīng)一個域(考慮到維護(hù)方便,也可以一個服務(wù)對應(yīng)多個域,沒有固定規(guī)則)

  • 如何確定上下文關(guān)系
  1. 遵從上下游關(guān)系:如,各個渠道的線索經(jīng)過篩選合并后,轉(zhuǎn)換為銷售機(jī)會,分配給具體的銷售人員進(jìn)行跟進(jìn),那么線索領(lǐng)域就是銷售機(jī)會領(lǐng)域的直接上游。這種就是比較好劃分的上下文,可以獨(dú)立迭代。
  2. 合作關(guān)系:兩個領(lǐng)域具有緊密的依賴關(guān)系,通常需要同時進(jìn)行開發(fā)維護(hù),通常劃分為一個上下文。如,對客戶資料的管理和對客戶跟進(jìn)關(guān)系的管理,通常需要一起維護(hù),拆分為多個上下文會增加維護(hù)成本,這就是基于系統(tǒng)架構(gòu)層面的劃分考慮。
  3. 混合關(guān)系:多業(yè)務(wù)混雜在一起,業(yè)務(wù)邊界模糊。如,常見的公海管理,剛開始只有線索公海,后面加入客戶公海,再加入銷售機(jī)會公海等等,那么這些公海是按業(yè)務(wù)拆分為獨(dú)立的子域,多個上下文,還是使用一個上下文統(tǒng)一維護(hù),具體實(shí)現(xiàn)只有團(tuán)隊(duì)內(nèi)部達(dá)成一致就行了。
  4. 技術(shù)復(fù)雜度:當(dāng)一個領(lǐng)域內(nèi)存在技術(shù)復(fù)雜度較高的部分時,也可以考慮單獨(dú)拆分為子域來進(jìn)行維護(hù)。比如:全局搜索,敏感詞過濾等等
image.png

DDD設(shè)計(jì)沒有標(biāo)準(zhǔn)答案,通常我們采用:事件風(fēng)暴 -> 命令風(fēng)暴 -> 尋找聚合 -> 子域劃分 -> 上下文劃分,這樣的流程去完成整個設(shè)計(jì),最終的目的是要參與設(shè)計(jì)的每個小伙伴,用統(tǒng)一的語言,達(dá)成對業(yè)務(wù)一致的認(rèn)知,研發(fā)、產(chǎn)品、領(lǐng)域?qū)<?對齊即可。


DDD項(xiàng)目總覽

DDD領(lǐng)域設(shè)計(jì),這里主要結(jié)合洋蔥架構(gòu)進(jìn)行使用

  • 洋蔥架構(gòu)

整潔架構(gòu)最主要的原則是依賴原則,它定義了各層的依賴關(guān)系,越往里依賴越低,代碼級別越高,越是核心能力。外圓代碼依賴只能指向內(nèi)圓,內(nèi)圓不需要知道外圓的任何情況。

image.png
  • golang 洋蔥架構(gòu)項(xiàng)目分層
application 
|- service     // 應(yīng)用服務(wù)
cmd         
|- server      // 服務(wù)啟動入口,main函數(shù)
common
|- dto         // 數(shù)據(jù)傳輸模型:應(yīng)用服務(wù)、領(lǐng)域服務(wù)的入?yún)⒍x
|- vo          // 請求響應(yīng)模型:應(yīng)用服務(wù)、領(lǐng)域服務(wù)的返回值定義
|- errors      // 業(yè)務(wù)錯誤的預(yù)定義
|- utils       // 基礎(chǔ)設(shè)施無關(guān)的工具類
domain
|- entity      // 領(lǐng)域模型(聚合根、實(shí)體、值對象)、防腐層模型、讀模型
|- factory     // 工廠方法,用于創(chuàng)建模型實(shí)例
|- event       // 領(lǐng)域事件
|- interfaces  // 對基礎(chǔ)實(shí)施依賴的接口約定
|- repository  // 對領(lǐng)域模型持久化的接口約定
|- service     // 領(lǐng)域服務(wù)(與應(yīng)用服務(wù)的區(qū)別是領(lǐng)域服務(wù)具有業(yè)務(wù)不變性,如果識別不了可以都寫到應(yīng)用服務(wù)中)
infrastructure 
|- config      // 配置的數(shù)據(jù)結(jié)構(gòu)與加載
|- controller  // 請求入口
    |- http       // HTTP請求入口,以及路由與Controller的綁定(北向網(wǎng)關(guān))
    |- grpc       // GRPC請求入口,以及路由與Controller的綁定(北向網(wǎng)關(guān))
|- driver      // 各種基礎(chǔ)設(shè)施客戶端的初始化
|- pubsub      // 事件訂閱入口,以及事件與訂閱函數(shù)的綁定(北向網(wǎng)關(guān))
|- persistence // 領(lǐng)域模型持久化的實(shí)現(xiàn),實(shí)現(xiàn)了領(lǐng)域模型到存儲模型的轉(zhuǎn)換與落庫(南向網(wǎng)關(guān))
|- serviceimpl // 對領(lǐng)域?qū)拥亩x接口的實(shí)現(xiàn),比如RPC請求
    |- httpclient // 接口實(shí)現(xiàn)方式為HTTP調(diào)用
    |- grpcclient // 接口實(shí)現(xiàn)方式為gRPC調(diào)用

  • 一個請求的執(zhí)行流程
image.png

可以看到,使用洋蔥架構(gòu)+領(lǐng)域模型的方式,通過依賴倒置,實(shí)現(xiàn)了各層級的解耦,領(lǐng)域服務(wù)和基礎(chǔ)設(shè)施的領(lǐng)域持久化實(shí)現(xiàn)也是通過接口訪問,互不影響。不會像傳統(tǒng)的MVC,一改需求從內(nèi)到外每個層級都需要改動。

優(yōu)點(diǎn):

  1. 層級解耦:應(yīng)用服務(wù)、領(lǐng)域服務(wù)、基礎(chǔ)設(shè)施,三層互不影響;洋蔥的的外層可以依賴內(nèi)層,內(nèi)層不能感知外層。
  2. 業(yè)務(wù)的不變性:可以將領(lǐng)域中具有不變性的部分抽離到domain service中,功能迭代時減小影響范圍。
  3. 可測試:核心的領(lǐng)域服務(wù)不依賴其他部分,可以方便的進(jìn)行單元測試,保證應(yīng)用的穩(wěn)定性。

缺點(diǎn):

  1. 模型轉(zhuǎn)換:為了層級間的解耦,會存在大量的模型轉(zhuǎn)換過程,比較耗時力(go 可以使用 copier 之類的包來完成不同結(jié)構(gòu)體,相同字段的映射,減輕轉(zhuǎn)換工作量)。
  2. 繁重:一個簡單的CRUD,需要寫很多業(yè)務(wù)無關(guān)的代碼,限制了洋蔥架構(gòu)的使用范圍。

大系統(tǒng)或者業(yè)務(wù)復(fù)制的系統(tǒng),每一個應(yīng)用節(jié)點(diǎn)都可以使用這樣的代碼分層方式;一些簡單節(jié)點(diǎn),可以選擇去掉領(lǐng)域?qū)?,也可以直接使用傳統(tǒng)的MVC,或者函數(shù)式編程,沒有必要將問題復(fù)雜化。領(lǐng)域設(shè)計(jì)不是萬能的,還是需要結(jié)合具體的業(yè)務(wù)選擇合適的架構(gòu)模型。

一份簡單的DDD示例代碼:transfer-money-go


參考:
一文帶你落地DDD
「DDD 戰(zhàn)略設(shè)計(jì)」實(shí)踐手冊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容