嘗試用大家都能聽得懂的話,結合我們在增值業務中的具體實作,分享一下我們從入門到實踐 DDD 的一些心得,
0. 寫在前面的
DDD(領域驅動設計)是 Eric Evans 于 2003 年提出的解決復雜的中大型軟體的方法,開始一直不慍不火,直到 Martin Fowler 于 2014 年發表的論文《Microservices》引起大家對微服務的關注,DDD 才重新慢慢的回到了大眾的視野中,
DDD 這幾年升溫的同時,也收到了很多行業人員對 DDD 的負面意見,主要原因大概有“晦澀難懂過于抽象”、“很難找到實際的案例參考”、“不知道怎么落地”等,
筆者在學習 DDD 的程序中,也遇到了這些問題,不過在經過幾個月的學習-實踐,逐漸掌握了 DDD 的一些思想后,感徑訓是確實有所受益,所以這里嘗試用白話去總結我們從入門到實踐的程序,盡量每一個概念都用我們的具體實作做出例子,希望能對想一起學習 DDD 的同事有所幫助,
1.一個維護中的業務系統引出的思考
我們后臺+前端大概 6-8 個開發同事這幾年一起維護了一個帶貨類的專案,這個專案我們用了最傳統的三層模型來搭建,大概是如下的模型:
當這個專案維護幾年之后,逐漸出些了一些有意思的情況,我挑選一些主要環節發現的代表性問題介紹下:
情況 1(代碼層面):少部分代碼可讀性在長期不同人員的修改下變得越來越差,如某個帶貨的核心 rpc 邏輯沒有任何嵌套平鋪在一個函式,單函式代碼行數達到幾百行,可讀性和維護性極差,成功化身為“技識訓城河”,
情況 2(微服務層面): 某些微服務初始職能劃分較為簡單,導致少量模塊在后續快速的迭代中快速膨脹,如其中的 mp 模塊,原本職能是用來承接 B 端門戶的功能,當我們決定拆分這個龐大的模塊時,這個模塊已經承載了 204 個 rpc,過多的能力承擔讓它編譯變慢、變成鏈路單點、改動較多、一旦出現問題影響較大,
情況 3(業務團隊層面):帶貨專案會使用一些其他業務系統的介面和資料結構,當這些業務系統想要修改這些介面和資料結構的時候 ,偶爾可能沒有察覺這里的依賴導致線上問題, 或者溝通過來發現耦合處比較多不容易改動,
對這個專案的維護引出了我們一些思考,在一個復雜業務系統中:代碼結構要如何設計、微服務的橫/縱向職能要如何劃分、業務團隊之間如何互動,才能持續在長期快速、多人協作的迭代中保證系統可維護性、拓展性、高內聚低耦合和穩定性,
而傳統的開發模式不管是面向程序(POP)還是面向物件(OOP)的思維,都沒辦法從微服務層面指導我們找到這些問題的答案,大概有兩種方法解決這個問題:
1)尋找一個總是有時間、總能做出正確決策的中心節點同事,介入每一處全域/細節的設計并統一做出決策,2)尋找一個新的規則/規范來做指導,讓每一位開發都能有做出正確決策的依據,在 Tencent 的氛圍和環境中,2)無疑是更合理的,所以我們想到了領域驅動設計(DDD),
2.DDD 的分層架構
DDD 最有標志性的一點,就是將傳統軟體設計三層模型轉化為了四層模型,這個轉化如下圖所示:
乍看之下,四層架構引入了很多概念,如領域服務、領域物件、 DTO、倉儲等等,我們先不用在意這些細節概念,因為下一節我們會逐個分析并列舉我們的實作例子,我們先關注這幾個關鍵的層:用戶界面層、應用層、領域層、基礎設施層,我們來看下他們的職能分工:
用戶界面層:網路協議的轉化/統一鑒權/Session 管理/限流配置/前置快取/例外轉換
應用層:業務流程編排(僅編排,不能存在業務邏輯)/ DTO 出入轉化
領域層:領域模型/領域服務/倉儲和防腐層的介面定義
基礎設施層:倉儲和防腐層介面實作/存盤等基礎層能力
這里必須要說的是,這四層不一定是指物理四層,也可以在一個微服務中拆分邏輯四層,四層架構有很多變種,如六邊形架構、洋蔥架構、整潔架構、清晰架構等等,這些繁多的概念我們這里不過多討論,僅以洋蔥架構為例,著重強調 DDD 中的依賴倒置(DIP),以便后面更容易介紹倉儲/防腐層等概念,
依賴倒置(DIP): 1.高級模塊不應依賴于低級模塊,兩者都應依賴抽象, 2.抽象不應依賴細節,細節應依賴于抽象,
如上,洋蔥架構越往里依賴越低,越是核心能力,基礎設施層在最外面,依賴其他層,這是是因為 DDD 中其他層等需要定義自己需要的基礎能力介面,而基礎設施層負責依賴并實作這些介面,從而實作整體依賴倒置,這體現了 DDD 的由全域入細微、自頂層向下層的設計思維,
3.DDD 的概念和實踐
1)戰略和戰術
DDD 的落地程序,其實就是戰略建模和戰術建模,
戰略建模,是指:通過 DDD 的理論,對業務需求進行拆解分析,劃分子域,梳理限界背景關系,通過領域語言從戰略層面進行領域劃分以及構建領域模型,并且在在構建領域模型的程序中梳理出業務對應的聚合、物體、以及值物件,
戰術建模,是指:以領域模型基礎,通過限界背景關系作為服務劃分的邊界進行微服務拆分,在每個微服務中進行領域分層,實作領域服務,從而實作領域模型對于代碼映射目的,最終實作 DDD 的落地實施,
當然,戰略和戰術的建模除了要考慮業務形態,還要考慮到組織架構,就如同康威定律中的表達,溝通架構會影響技術架構,
康威定律:任何組織在設計一套系統(廣義概念上的系統)時,所交付的設計方案在結構上都與該組織的溝通結構保持一致,
2)領域
DDD 在解決復雜的問題的時候,使用的是分而治之的思想,而這個分而治之的思想,就是從領域開始,一個領域就是一個問題空間,而我們在拆分這個問題空間的時候,也就是在劃分子領域和尋找它的解系統的程序,
實踐例子:
如我們某個新的增值業務,就是看成是的大的增值業務域,接下來我們通過 DDD 來指導拆分它,
3)子域
如果一個領域太大太復雜,涉及到的業務規則、互動流程、領域概念太多,就不能直接針對這個大的領域進行建模,這時就需要將領域進行拆分,本質上就是把大問題拆分為小問題,把一個大的領域劃分為了多個小的領域(子域),
子域可以分為三類:
核心子域:業務成功的核心競爭力,
通用子域:不是核心,但被整個業務系統所使用 ,
支撐子域:不是核心,不被整個系統使用,完成業務的必要能力,
子域的劃分除了分治了大的問題空間,也劃定了作業的優先級,我們應該給予核心域最高的優先級和最大的資源,在實施 DDD 的程序中,我們也是主要關注于核心域,
實踐例子:
子域的劃分,需要比較強的業務知識和產品研發集體討論,準確和深入的業務見解在這一階段尤為重要,這里我們不對業務知識深入討論,僅展示下我們的對增值業務域的拆解結果,
這里要說的是,套餐域在實作的程序中由于產品需求變化概念被廢棄了,但是由于我們的子域拆分,套餐域和其他域實作上沒有任何耦合,所以廢棄套餐域概念的廢棄就像拆掉一個積木一樣,對整套系統沒有任何影響,也不會遺留任何不必要的包袱代碼,
4)限界背景關系
要理解限界背景關系,首先要先介紹通用語言,通用語言是 DDD 非常重要的一點,比如商品這個概念,在商品域里是指備上架的商品, 包含了 id、介紹、檔案等,在交易域里其實是指訂單中被交易的物體,關注的是 id、成交時刻的售價等引數、成交數量,而如果不能明確這些概念和他們的關系就會讓開發人員的實作變的隨心所欲和模糊,
而限界背景關系是就是劃分一個邊界,當領域模型被一個顯示的邊界所包圍時,其中每個概念的含義應該是明確且有唯一的含義,
我覺得初學者最常碰到的問題,肯定"明明已經有子域了,為什么還會有限界背景關系這個概念",子域是一個子問題空間,而限界背景關系的作用是指導如何設計這個問題空間的解系統,換句話說,限界背景關系才是真正用來指導微服務劃分,一般來說一個子域對應一個或多個限界背景關系,
劃分限界背景關系可以參考如下的規則:1) 概念是否有歧義:如果一個模型在一個背景關系里面有歧義,就說明可以繼續拆分限界背景關系,
2)外部系統:可以把與外部系統互動的那部分拆分出去降低外部系統對我們我們的核心業務邏輯的影響,
3)組織架構:不同團隊最好在不同的限界背景關系里面開發,避免溝通不順暢、集成困難等問題,可以參考上述"康威定律",
實踐例子 1:
如上所述,商品這個概念,是需要用限界背景關系在不同場景區分開的,當然這也會導致兩個限界背景關系之間會有依賴,通過 DDD 的概念可以指導我們進行如下實作,
其中 gateway/gatewayimpl 是防腐層的實作,DTO 是指資料傳輸物件,APP 是指商品應用層,兩個不同顏色的商品是指兩個背景關系中分別進行定義的不同的物體或值物件,
實踐例子 2:
交易域中,有兩個訂單的概念,其中第一個訂單的概念是指業務層訂單, 第二個訂單的概念是指內部基礎層訂單,業務訂單更關注發生交易的成交商品資訊,這個訂單是用戶需要的,基礎層訂單更關注交易底層的程序資訊,這個訂單更多是我們內部人員需要的,用戶不理解,
當時有個思路是想讓基礎層團隊的同學額外開發直接支持基礎層訂單存盤業務資訊,這明顯是不符合 DDD 限界背景關系劃分規則 1)和 3)的,是需要通過限界背景關系解耦開的,所以我們在交易域中拆分兩個背景關系,后續從微服務層面也是相互獨立的微服務,各自管理各自的領域物體和值物件,
5)防腐層
當兩個限界背景關系相互呼叫的時候,需使用防腐層(ACL)來進行兩個限界背景關系的隔離,并實作 value object 的轉換,避免不同背景關系直接互相呼叫,不然一旦被呼叫背景關系被修改則可能產生較大影響,
實踐例子:
實作鏈路可以參考 3.4 的例子 1,在商品域中,我們的防腐層是按照如下的目錄方式實作的, 領域層來定義領域層需要的防腐介面,基礎設施層繼承并實作防腐介面,在基礎設施層直接呼叫其他限界背景關系,
productdomainsvr (商品限界背景關系) ├── domain(領域層) │ ├── aggregate │ │ ├── spu.cpp //1)spu領域物件需要呼叫其他限界背景關系生成id │ │ └── spu.h │ └── gateway │ └── gen_id_gateway.h //2)領域層定義呼叫其他限界背景關系生成id的防腐介面 ├── infrastructure(基礎設施層) │ └── gatewayimpl │ └── acl(防腐層) │ ├── gen_id_gateway_impl.cpp //3)基礎設施層實作領域層定義的防腐介面,真實呼叫其他背景關系 │ └── gen_id_gateway_impl.h
6)領域事件
兩個限界背景關系除了通過使用防腐層直接呼叫,更多的時候是通過領域事件來進行解耦,
并不是所有領域中發生的事情都需要被建模為領域事件,我們只關注有業務價值的事情,領域事件是領域專家所關心的(需要跟蹤的、希望被通知的、會引起其他模型物件改變狀態的)發生在領域中的一些事情,
其實,領域事件的本質就是事件,我們常見的 kafka、wq 等都可以作為領域事件的實作基建,通過領域事件,可以把很輕松兩個限界背景關系解耦
實踐例子:
在我們的增值業務中,交易域的"支付成功"就是一個領域事件,計費域訂閱這個領域事件,從而可以根據這個事件調整客戶的計費資源包物體,
可以想象,如果這里沒有采用領域事件, 而是交易域直接呼叫計費域的 rpc 通知交易成功,那么當后續有其他域需要接受“支付成功”這個事件,或者,計費域被呼叫的介面出現故障,都會讓交易域陷入麻煩,前者需要交易域不停的堆疊呼叫外部 rpc 的代碼并讓系統變得不穩定,后者則直接會讓計費域的故障影響到用戶交易,
7)物體/值物件
物體是指背景關系中唯一的且可持續變化的基礎單元,在其生命周期中可以通過穩定的唯一 id 來標識,物體在我們代碼中以領域物件的形態存在,同時具備屬性和方法,物體是 DDD 用來實作充血編程、解決貧血癥的關鍵,
與物體相對應的就是值物件,如果沒有唯一標識就是值物件,值物件一般是嵌套在物體里面的,
實踐例子:
商品域中的物體和值物件如下
| 物體 | 描述 | 關鍵值物件 |
|---|---|---|
| SPU | 指一個被上架的服務, | spu_id, spu_type,狀態等, |
| SKU | 指一個服務具體的單項套餐, | sku_id, 規格,價格等, |
| 折扣 | 自定義折扣, | 折扣 id,折扣型別,折扣比例等, |
8)聚合/聚合根
把關系緊密的物體放到一個聚合中,每個聚合中有一個物體作為聚合根,所有對于聚合內物件的訪問都通過聚合根來進行,外部物件只能持有對聚合根的參考,每個聚合都可以有一個獨立的背景關系邊界,
聚合應劃分的盡量小,一個聚合只包含一個聚合根物體和密不可分的物體,物體中只包含最小數量的屬性,設計這樣的小聚合有助于進行后續微服務的拆分,
如果一個 rpc 所實作的功能是跨聚合的,那跨聚合的編排協調作業應該放在應用層來實作,
實踐例子:
我們可以在 6)中的例子劃分如下的聚合,
| 聚合 | 物體 | 是否是根 |
|---|---|---|
| 聚合 1 | 服務 SPU | 是 |
| 服務 SKU | 否 | |
| 聚合 2 | 折扣 | 是 |
在底層存盤落表上, spu 物體/折扣物體作為表的一行, 而 sku 物體在這種聚合建模的指引下我們設計成 spu 聚合根的一列,
在微服務拆分上,如果想拆到最細粒度, 可以把兩個聚合按照各自背景關系拆成獨立的微服務,當然這種落地實作并不是 DDD 強行要求的,我認為一些時候我們也可以從開發維護效率的角度考慮, 將一些有關聯的小背景關系放在一個為微服務上,我們在處理商品域上選擇了后者,
9)DTO/領域物件/Data object
當一個請求進入 DDD 所設計的系統中,這個請求的形態會根據所在的層級發生如下變換,DTO<->領域物件<->Data object,
DTO 是指對外傳輸的其他服務需要理解的結構,領域物件是指同時包含了屬性和方法的領域物體封裝,Data object 則是真正用于最終存盤的資料結構,
這里其實很容易發現,DTO 的存在雖然符合其他呼叫方最少知識原則(LKP),但如果連最簡單的查詢請求都需要做這三級的轉換,那無疑是會加重開發的復雜度,變成為了設計模式而設計模式,
最少知識原則(迪米特法則,LKP):一個軟體物體應當盡可能少地與其他物體發生相互作用,這里的軟體物體是一個廣義的概念,不僅包括物件,還包括系統、類、模塊、函式、變數等,
所以 DDD 在這里一般會使用 CQRS(讀寫責任分離)架構,來保證一些簡單的查詢請求不會因為領域建模而變得過于復雜,CQRS(讀寫責任分離)基于 CQS(讀寫分離),使用了 CQRS 的 DDD 物件轉換流程如下:
實踐例子:
我們的實作是在領域物件中封裝了轉換的 convert 函式(當然也可以在基礎設施層將 convert 方法拆分出來做單獨的封裝),用于將 DTO 轉換為領域物件,或者將領域物件轉換為 DO,下面是我們明細域的實際轉換代碼和轉換程序,
//1.領域物件中定義convert方法 class DetailRecord { public: int ConvertFromDTO(const google::protobuf::Message& oDto); int ConvertToDO(detailrecordinfrastructure::DetailRecordDO & oDo); /*...*/ }; //2.應用層呼叫方法將DTO轉化為領域物件, 然后呼叫倉儲介面進行持久化 int DetailrecordApplication::InsertDetailRecord(unsigned int head_uin, const InsertDetailRecordReq& req, InsertDetailRecordResp* resp) { int iRet = 0; class DetailRecord oRecord; iRet = oRecord.ConvertFromDTO(req); //生成領域物件,可以同時利用領域物件的方法進行自檢等操作 /*...*/ iRet = m_oDetailRecordGateway->Save(oRecord); //呼叫倉儲介面進行持久化 /*...*/ return iRet; } //3.在倉儲中將領域物件轉化為Dataobject,進行落存盤操作,并發布領域事件 int DetailRecordGatewayImpl::Save(DetailRecord & oEntity){ detailrecordinfrastructure::DetailRecordDO oDo; int iRet = oEntity.ConvertToDO(oDo); /*...*/ iRet = oKvMapper.insert(oDo); //實際落存盤 /*...*/ iRet = oEventMapper.publish(oDo); //發送領域事件 /*...*/ return iRet; }
10)倉儲
倉儲是領域層由定義介面,它抽象了業務邏輯中對物體的訪問(包括讀取和存盤)的技術細節,它的作用就是通過隔離具體的存盤層技術實作來保證業務邏輯的穩定性,注意,倉儲只是介面的定義是在領域層,但是它的實作是在基礎設施層,
倉儲不是資料庫 Dao!!!
倉儲不是資料庫 Dao!!!
倉儲不是資料庫 Dao!!!
重要的事情說三遍,倉儲是從業務邏輯的角度抽象出來的介面,所以倉儲的介面在實作上,一般是一個聚合對應一個倉儲實作,倉儲的需要用領域物件做引數,倉儲介面的命名也可以取 save 這種更業務的命名, 而避免傳統 dao 的 insert/set 等這種明明,
實踐例子:
通過 3.9 的例子,我們可以發現,倉儲用于持久化的介面里,不但包含了寫 kv 的操作,還包含了發布領域事件等操作,這就是因為倉儲是從業務邏輯角度抽象出來的介面,領域層只需要理解 save 這個業務操作,而不應該理解 save 的程序包含了落存盤、發布領域事件等具體流程,
//1.領域層定義DetailRecord倉儲的介面 class DetailRecordGateway { public: /*...*/ virtual int Save(DetailRecord & oEntity) = 0; /*...*/ }; //2.基礎設施層繼承領域層的倉儲介面進行實作 class DetailRecordGatewayImpl : public DetailRecordGateway { public: /*...*/ virtual int Save(DetailRecord & oEntity); /*...*/ }; //3.倉儲save介面具體實作 int DetailRecordGatewayImpl::Save(DetailRecord & oEntity){ detailrecordinfrastructure::DetailRecordDO oDo; int iRet = oEntity.ConvertToDO(oDo); /*...*/ iRet = oKvMapper.insert(oDo); //實際落存盤 /*...*/ iRet = oEventMapper.publish(oDo); //發布領域事件 /*...*/ return iRet; }
11)領域服務
當一些能力不適合放在某個領域物件中實作,又因為過于復雜不應該放在應用層來實作,可以把這些操作封裝成領域服務的中方法,由應用層編排領域層的領域物件和領域服務方法來完成具體的業務功能,
4.DDD 的代碼腳手架
我們基于對 DDD 的理解和 WXG 的 svrkit 框架,設定我們的代碼腳手架,腳手架的目錄如下所示,希望可以給想一起實踐的同事拋磚引玉,也歡迎大家來找我們一起討論:
專案目錄
├── adapter(物理用戶界面模塊)
├── domainsvr(領域微服務)
│ ├── detailrecorddomainsvr(明細域微服務)
│ │ ├── adapter(用戶界面層)
│ │ ├── application(應用層)
│ │ │ ├── detailrecord_application.cpp(應用層方法)
│ │ ├── domain(領域層)
│ │ │ ├── aggregate(聚合根)
│ │ │ │ ├── detail_record.cpp(領域物件)
│ │ │ │ └── detailrecordaggregate.proto(聚合根的值物件)
│ │ │ ├── entity(非根物體)
│ │ │ │ └── detailrecordentity.proto(非根物體的值物件)
│ │ │ ├── gateway
│ │ │ │ └── detail_record_gateway.h(倉儲介面)
│ │ │ └── detailrecord_domain_service.cpp(領域服務)
│ │ ├── infrastructure(基礎設施層)
│ │ │ ├── gatewayimpl
│ │ │ │ ├── acl(防腐層實作)
│ │ │ │ └── detail_record_gateway_impl.cpp(倉儲實作)
│ │ │ └── detailrecordinfrastructure.proto(Data object定義)
│ │ └── detailrecord.proto(DTO定義)
└── infrastructuresvr(物理基礎設施模塊)
作者:kunqian
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/Advanced-background-development_vernacular-DDD-from-introduction-to-practice.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/540823.html
標籤:其他
