談?wù)劥a:降低復(fù)雜度,從放棄三層架構(gòu)到DDD入門
本文首發(fā)于泊浮目的簡書:www.jianshu.com/u/204b8aaab…
| 版本 | 日期 | 備注 |
|---|---|---|
| 1.0 | 2021.8.1 | 文章首發(fā) |
1.前言
最近我發(fā)現(xiàn)團(tuán)隊(duì)項(xiàng)目中的某個(gè)應(yīng)用復(fù)雜度越來越高,具體表現(xiàn)為:
代碼可讀性較差:各個(gè)服務(wù)之間調(diào)用復(fù)雜,流程不清晰
修改部分業(yè)務(wù)導(dǎo)致大量測(cè)試用例失敗,但很難快速的尋找出這些測(cè)試用例失敗的根因
基于這些情況,我開始尋找降低復(fù)雜度的方案,于是就有了這篇再談DDD的文章。
1.1 具體問題
1.1.1 宏觀角度
從宏觀來說,軟件架構(gòu)模式演進(jìn)經(jīng)歷了三個(gè)階段。

第一階段是單機(jī)架構(gòu):采用面向過程的設(shè)計(jì)方法,系統(tǒng)包括客戶端 UI 層和數(shù)據(jù)庫兩層,采用 C/S 架構(gòu)模式,整個(gè)系統(tǒng)圍繞數(shù)據(jù)庫驅(qū)動(dòng)設(shè)計(jì)和開發(fā),并且總是從設(shè)計(jì)數(shù)據(jù)庫和字段開始。
第二階段是集中式架構(gòu):采用面向?qū)ο蟮脑O(shè)計(jì)方法,系統(tǒng)包括業(yè)務(wù)接入層、業(yè)務(wù)邏輯層和數(shù)據(jù)庫層,采用經(jīng)典的三層架構(gòu),也有部分應(yīng)用采用傳統(tǒng)的 SOA 架構(gòu)。這種架構(gòu)容易使系統(tǒng)變得臃腫,可擴(kuò)展性和彈性伸縮性差。
第三階段是分布式微服務(wù)架構(gòu):隨著微服務(wù)架構(gòu)理念的提出,集中式架構(gòu)正向分布式微服務(wù)架構(gòu)演進(jìn)。微服務(wù)架構(gòu)可以很好地實(shí)現(xiàn)應(yīng)用之間的解耦,解決單體應(yīng)用擴(kuò)展性和彈性伸縮能力不足的問題。我們知道,在單機(jī)和集中式架構(gòu)時(shí)代,系統(tǒng)分析、設(shè)計(jì)和開發(fā)往往是獨(dú)立、分階段割裂進(jìn)行的。
比如,在系統(tǒng)建設(shè)過程中,我們經(jīng)常會(huì)看到這樣的情形:A 負(fù)責(zé)提出需求,B 負(fù)責(zé)需求分析,C 負(fù)責(zé)系統(tǒng)設(shè)計(jì),D 負(fù)責(zé)代碼實(shí)現(xiàn),這樣的流程很長,經(jīng)手的人也很多,很容易導(dǎo)致信息丟失。最后,就很容易導(dǎo)致需求、設(shè)計(jì)與代碼實(shí)現(xiàn)的不一致,往往到了軟件上線后,我們才發(fā)現(xiàn)很多功能并不是自己想要的,或者做出來的功能跟自己提出的需求偏差太大。
而且在單機(jī)和集中式架構(gòu)這兩種模式下,軟件無法快速響應(yīng)需求和業(yè)務(wù)的迅速變化,最終錯(cuò)失發(fā)展良機(jī)。此時(shí),分布式微服務(wù)的出現(xiàn)就有點(diǎn)恰逢其時(shí)的意思了。
上面這部分來自于極客時(shí)間,這里面指出一般DDD是使用在微服務(wù)設(shè)計(jì)與拆分上,但我認(rèn)為在單體應(yīng)用中做模塊的拆分也是可以并推薦的,這可以讓你的模塊在需要時(shí)可以即刻拆分出去——變成一個(gè)獨(dú)立的微服務(wù)。相關(guān)可以參考【ZStack】4.進(jìn)程內(nèi)服務(wù),這是一個(gè)開源,并實(shí)施于生產(chǎn)中很好的一個(gè)案例。
1.1.2 微觀角度
這個(gè)問題很簡單,service的代碼必然會(huì)越堆越多,而且聚攏越來越多的業(yè)務(wù)。

2.DDD入門
我們先來看一張圖:
從最外層開始——什么是領(lǐng)域?大白話來說就是一系列問題的聚合。舉個(gè)例子:
電商平臺(tái)中的電商域,你要解決的一系列問題有:
用戶認(rèn)證
移動(dòng)收付
訂單
報(bào)價(jià)
...
可以看到,域是呈現(xiàn)出來的是一系列的業(yè)務(wù)領(lǐng)域問題。
在不同域中,同一個(gè)數(shù)據(jù)實(shí)體的抽象形態(tài)往往是不同的。比如,Bookstore 應(yīng)用中的書本,在銷售領(lǐng)域中關(guān)注的是價(jià)格,在倉儲(chǔ)領(lǐng)域中關(guān)注的是庫存數(shù)量,在商品展示領(lǐng)域中關(guān)注的是書籍的介紹信息。
2.1 上下文邊界
往里面,我們應(yīng)該看到的是限界上下文。其實(shí)這個(gè)翻譯并不好,原文叫bounded context,叫做上下文邊界更為妥當(dāng)。本質(zhì)上來說,它定義了邊界。再具體點(diǎn),即:用來封裝通用語言和領(lǐng)域?qū)ο?,提供上下文環(huán)境,保證在領(lǐng)域之內(nèi)的一些術(shù)語、業(yè)務(wù)相關(guān)對(duì)象等(通用語言)有一個(gè)確切的含義,沒有二義性。
2.2 聚合
接下來,我們看到了聚合。聚合就是由業(yè)務(wù)和邏輯緊密關(guān)聯(lián)的實(shí)體和值對(duì)象組合而成的,聚合是數(shù)據(jù)修改和持久化的基本單元,每一個(gè)聚合對(duì)應(yīng)一個(gè)倉儲(chǔ),實(shí)現(xiàn)數(shù)據(jù)的持久化。
聚合有一個(gè)聚合根和上下文邊界,這個(gè)邊界根據(jù)業(yè)務(wù)單一職責(zé)和高內(nèi)聚原則,定義了聚合內(nèi)部應(yīng)該包含哪些實(shí)體和值對(duì)象,而聚合之間的邊界是松耦合的。按照這種方式設(shè)計(jì)出來的微服務(wù)很自然就是“高內(nèi)聚、低耦合”的。
那聚合根是什么呢?
聚合根的主要目的是為了避免由于復(fù)雜數(shù)據(jù)模型缺少統(tǒng)一的業(yè)務(wù)規(guī)則控制,而導(dǎo)致聚合、實(shí)體之間數(shù)據(jù)不一致性的問題。
傳統(tǒng)數(shù)據(jù)模型中的每一個(gè)實(shí)體都是對(duì)等的,如果任由實(shí)體進(jìn)行無控制地調(diào)用和數(shù)據(jù)修改,很可能會(huì)導(dǎo)致實(shí)體之間數(shù)據(jù)邏輯的不一致。而如果采用鎖的方式則會(huì)增加軟件的復(fù)雜度,也會(huì)降低系統(tǒng)的性能。
如果把聚合比作組織,那聚合根就是這個(gè)組織的負(fù)責(zé)人。聚合根也稱為根實(shí)體,它不僅是實(shí)體,還是聚合的管理者。
首先它作為實(shí)體本身,擁有實(shí)體的屬性和業(yè)務(wù)行為,實(shí)現(xiàn)自身的業(yè)務(wù)邏輯。
其次它作為聚合的管理者,在聚合內(nèi)部負(fù)責(zé)協(xié)調(diào)實(shí)體和值對(duì)象按照固定的業(yè)務(wù)規(guī)則協(xié)同完成共同的業(yè)務(wù)邏輯。
最后在聚合之間,它還是聚合對(duì)外的接口人,以聚合根 ID 關(guān)聯(lián)的方式接受外部任務(wù)和請(qǐng)求,在上下文內(nèi)實(shí)現(xiàn)聚合之間的業(yè)務(wù)協(xié)同。也就是說,聚合之間通過聚合根 ID 關(guān)聯(lián)引用,如果需要訪問其它聚合的實(shí)體,就要先訪問聚合根,再導(dǎo)航到聚合內(nèi)部實(shí)體,外部對(duì)象不能直接訪問聚合內(nèi)實(shí)體。
2.3 實(shí)體與值對(duì)象
在 DDD 中有這樣一類對(duì)象,它們擁有唯一標(biāo)識(shí)符,且標(biāo)識(shí)符在歷經(jīng)各種狀態(tài)變更后仍能保持一致。對(duì)這些對(duì)象而言,重要的不是其屬性,而是其延續(xù)性和標(biāo)識(shí),對(duì)象的延續(xù)性和標(biāo)識(shí)會(huì)跨越甚至超出軟件的生命周期。我們把這樣的對(duì)象稱為實(shí)體。其實(shí)很像數(shù)據(jù)庫里自帶不變id的一行行業(yè)務(wù)數(shù)據(jù)。
值對(duì)象相對(duì)不是那么重要,因?yàn)樗怯脕砻枋鰧?shí)體的一組屬性集。很多系統(tǒng)中的實(shí)現(xiàn)會(huì)以json來實(shí)現(xiàn),比如【ZStack】7.標(biāo)簽系統(tǒng)。
為了方便理解,這邊做個(gè)小結(jié)。實(shí)體和值對(duì)象的目的都是抽象聚合若干屬性以簡化設(shè)計(jì)和溝通,有了這一層抽象,我們?cè)谑褂萌藛T實(shí)體時(shí),不會(huì)產(chǎn)生歧義,在引用地址值對(duì)象時(shí),不用列舉其全部屬性,在同一個(gè)限界上下文中,大幅降低誤解、縮小偏差,兩者的區(qū)別如下:
兩者都經(jīng)過屬性聚類形成,實(shí)體有唯一性,值對(duì)象沒有。在本文案例的限界上下文中,人員有唯一性,一旦某個(gè)人員被系統(tǒng)納入管理,它就被賦予了在事件、流程和操作中被唯一識(shí)別的能力,而值對(duì)象沒有也不必具備唯一性。
實(shí)體著重唯一性和延續(xù)性,不在意屬性的變化,屬性全變了,它還是原來那個(gè)它;值對(duì)象著重描述性,對(duì)屬性的變化很敏感,屬性變了,它就不是那個(gè)它了(意味著不可變性,它可能是從外部查詢來的)。
戰(zhàn)略上的思考框架穩(wěn)定不變,戰(zhàn)術(shù)上的模型設(shè)計(jì)卻靈活多變,實(shí)體和值對(duì)象也有可能隨著系統(tǒng)業(yè)務(wù)關(guān)注點(diǎn)的不同而更換位置。比如,如果換一個(gè)特殊的限界上下文,這個(gè)上下文更關(guān)注地址,而不那么關(guān)注與這個(gè)地址產(chǎn)生聯(lián)系的人員,那么就應(yīng)該把地址設(shè)計(jì)成實(shí)體,而把人員設(shè)計(jì)成值對(duì)象。
3. DDD上手
3.1 從三層模型到DDD
這里先簡單介紹一下三層模型到DDD對(duì)應(yīng)的一個(gè)變化。
可以的看得出來,主要是對(duì)service進(jìn)行了拆分。一般可以拆成三層:
應(yīng)用服務(wù)層:多個(gè)領(lǐng)域服務(wù)或外部應(yīng)用服務(wù)進(jìn)行封裝、編排和組合,對(duì)外提供粗粒度的服務(wù)。應(yīng)用服務(wù)主要實(shí)現(xiàn)服務(wù)組合和編排,是一段獨(dú)立的業(yè)務(wù)邏輯。
領(lǐng)域服務(wù)層:由多個(gè)實(shí)體組合而成,一個(gè)方法可能會(huì)跨實(shí)體進(jìn)行調(diào)用。在代碼過于復(fù)雜的時(shí)候,可以將每個(gè)領(lǐng)域服務(wù)拆分為一個(gè)領(lǐng)域服務(wù)類,而不是將所有領(lǐng)域服務(wù)代碼放到一個(gè)領(lǐng)域服務(wù)類中。
實(shí)體:是一個(gè)充血模型。同一個(gè)實(shí)體相關(guān)的邏輯都在實(shí)體類代碼中實(shí)現(xiàn)。
3.2 建模簡介
我們可以用三步來劃定領(lǐng)域模型和微服務(wù)的邊界。
第一步:在事件風(fēng)暴中梳理業(yè)務(wù)過程中的用戶操作、事件以及外部依賴關(guān)系等,根據(jù)這些要素梳理出領(lǐng)域?qū)嶓w等領(lǐng)域?qū)ο蟆?/p>
第二步:根據(jù)領(lǐng)域?qū)嶓w之間的業(yè)務(wù)關(guān)聯(lián)性,將業(yè)務(wù)緊密相關(guān)的實(shí)體進(jìn)行組合形成聚合,同時(shí)確定聚合中的聚合根、值對(duì)象和實(shí)體。在第二章的圖里,聚合之間的邊界是第一層邊界,它們?cè)谕粋€(gè)微服務(wù)實(shí)例中運(yùn)行,這個(gè)邊界是邏輯邊界,所以用虛線表示。
第三步:根據(jù)業(yè)務(wù)及語義邊界等因素,將一個(gè)或者多個(gè)聚合劃定在一個(gè)限界上下文內(nèi),形成領(lǐng)域模型。在第二章的圖里,限界上下文之間的邊界是第二層邊界,這一層邊界可能就是未來微服務(wù)的邊界,不同限界上下文內(nèi)的領(lǐng)域邏輯被隔離在不同的微服務(wù)實(shí)例中運(yùn)行,物理上相互隔離,所以是物理邊界,邊界之間用實(shí)線來表示。
3.3 實(shí)踐:設(shè)計(jì)一個(gè)MiniStack
為了便于大家理解,我在這里會(huì)設(shè)計(jì)一個(gè)很簡單的Iaas平臺(tái),并在里面代入最基本的DDD概念。
3.3.1 產(chǎn)品愿景
為了:企業(yè)的內(nèi)部的開發(fā)者、運(yùn)維人員
他們的:計(jì)算、存儲(chǔ)、網(wǎng)絡(luò)資源管理
這個(gè):MiniStack
是一個(gè):私有云平臺(tái)
它可以:管理計(jì)算、存儲(chǔ)、網(wǎng)絡(luò)資源管理,幫用戶簡單快速的創(chuàng)建虛擬機(jī)
而不像:OpenStack
我們的產(chǎn)品:簡單、健壯、智能
串起來就是:為了滿足企業(yè)的內(nèi)部的開發(fā)者和運(yùn)維人員,他們的硬件資源管理,我們建設(shè)里這個(gè)MiniStack,它是一個(gè)私有云平臺(tái),它可以管理計(jì)算、存儲(chǔ)、網(wǎng)絡(luò)資源管理,幫用戶簡單快速的創(chuàng)建虛擬機(jī),而不像OpenStack,我們的產(chǎn)品簡單、健壯、彈性。
3.3.2 場景分析
因篇幅原因,我們來聊個(gè)最典型的場景——?jiǎng)?chuàng)建虛擬機(jī),以便理出相關(guān)的領(lǐng)域模型。
在這里我們需要注意,我們要盡可能的梳理整個(gè)系統(tǒng)發(fā)生的操作、命令、領(lǐng)域時(shí)間以及依賴變化等。
3.3.2.1 創(chuàng)建虛擬機(jī)
用戶登陸系統(tǒng):從數(shù)據(jù)庫中對(duì)信息進(jìn)行校驗(yàn),完成登陸認(rèn)證
創(chuàng)建虛擬機(jī):填寫虛擬機(jī)名、集群、計(jì)算規(guī)格、L3網(wǎng)絡(luò)以及鏡像。如果需要的話(簡單的體現(xiàn)),可以指定所在的物理機(jī)、以及網(wǎng)段。
VM服務(wù)需要提供創(chuàng)建虛擬機(jī)接口
提交至MiniStack引擎,引起開始做相關(guān)調(diào)度:
VM服務(wù)需要提供啟動(dòng)接口
物理機(jī)服務(wù)需要提供拉取鏡像接口
網(wǎng)絡(luò)服務(wù)需要提供IP分配接口
物理機(jī)服務(wù)需要提供查詢接口
尋找符合計(jì)算、存儲(chǔ)資源的低負(fù)載物理機(jī),并更新vm所屬的物理機(jī)
分配L3網(wǎng)絡(luò)中的空閑IP,并更新vm相關(guān)的網(wǎng)絡(luò)信息
告訴物理機(jī)agent:從鏡像服務(wù)器拉取鏡像到第1步尋找出的物理機(jī)
告訴物理機(jī)agent啟動(dòng)參數(shù),拉起vm
界面上返回創(chuàng)建成功,用戶可以看到vm
但創(chuàng)建完虛擬機(jī)以后并不是就這么完事了,萬一哪天這臺(tái)物理機(jī)carsh了呢?哪天CPU因?yàn)槠婀值倪M(jìn)程而打滿了呢?因此為了我們的目標(biāo)——智能,創(chuàng)建vm后,MiniStac每5分鐘收集一系列的監(jiān)控信息:
向物理機(jī)agent發(fā)送心跳包,確保物理機(jī)狀態(tài)正常
向虛擬機(jī)agent發(fā)送心跳包,并會(huì)返回:計(jì)算、存儲(chǔ)、網(wǎng)絡(luò)的相關(guān)狀態(tài)
3.3.3 宏觀設(shè)計(jì):領(lǐng)域建模
在這一步,我們需要對(duì)業(yè)務(wù)進(jìn)行分析,建立領(lǐng)域模型。一般步驟為:
找出領(lǐng)域?qū)嶓w和值對(duì)象等領(lǐng)域?qū)ο?/p>
找出聚合根,根據(jù)實(shí)體、值對(duì)象與聚合根的依賴關(guān)系,建立聚合
第三步根據(jù)業(yè)務(wù)及語義邊界等因素,定義限界上下文
3.3.3.1 定義實(shí)體
我們大致可以找出幾個(gè)實(shí)體:
虛擬機(jī)
啟動(dòng)
停止
物理機(jī)的存儲(chǔ)資源
查詢
分配
釋放
物理機(jī)的計(jì)算資源
查詢
分配
釋放
L3網(wǎng)絡(luò)
分配IP
鏡像服務(wù)器
查詢鏡像
添加鏡像
發(fā)布鏡像
3.3.3.2 定義聚合與限界上下文
在找聚合前,我們先要找出聚合根??梢苑譃槲锢頇C(jī)、網(wǎng)絡(luò)、鏡像服務(wù)器、虛擬機(jī)。而他們彼此都是獨(dú)立的上下文,在需要的情況下,也可以拆成一個(gè)個(gè)微服務(wù),如果是單體應(yīng)用,則建議用模塊手段進(jìn)行邏輯隔離。

3.3.4 微觀:領(lǐng)域?qū)ο笈c代碼結(jié)構(gòu)分析
當(dāng)我們完成宏觀上的建模后,便可以開始做微觀的事:梳理微服務(wù)內(nèi)的領(lǐng)域?qū)ο?,梳理領(lǐng)域?qū)ο笾g的關(guān)系,確定它們?cè)诖a模型和分層架構(gòu)中的位置,建立領(lǐng)域模型與微服務(wù)模型的映射關(guān)系,以及服務(wù)之間的依賴關(guān)系。
大致上,分位兩步:
分析領(lǐng)域?qū)ο?/p>
設(shè)計(jì)代碼結(jié)構(gòu)
3.3.4.1 分析領(lǐng)域?qū)ο?/h4>
在這一步,我們需要確認(rèn):
服務(wù)的分層
應(yīng)用服務(wù)由哪些服務(wù)組成
領(lǐng)域服務(wù)包含哪些實(shí)體和實(shí)體方法
哪個(gè)實(shí)體是聚合根
實(shí)體有哪些屬性和方法
哪些對(duì)象為值對(duì)象
由于我們的用例比較簡單,整理如下:
應(yīng)用服務(wù):
VM創(chuàng)建服務(wù):負(fù)責(zé)創(chuàng)建VM,會(huì)調(diào)度大量的底層領(lǐng)域服務(wù)
領(lǐng)域服務(wù):VM服務(wù)、物理機(jī)服務(wù)、網(wǎng)絡(luò)服務(wù)、鏡像服務(wù)
VM服務(wù):管理VM的生命周期,如創(chuàng)建、刪除、啟動(dòng)、停止等
物理機(jī)服務(wù):物理機(jī)相關(guān)服務(wù),如添加、刪除、狀態(tài)變更、心跳感知、資源RUD等
網(wǎng)絡(luò)服務(wù):網(wǎng)絡(luò)相關(guān)服務(wù),如創(chuàng)建刪除L2、L3網(wǎng)絡(luò),IP管理等
鏡像服務(wù):鏡像服務(wù)器相關(guān)服務(wù),如添加、刪除、狀態(tài)變更、增加鏡像等
實(shí)體:VM實(shí)體、物理機(jī)實(shí)體、本地存儲(chǔ)實(shí)體(物理機(jī)存儲(chǔ))
VM實(shí)體:啟動(dòng)、停止等
物理機(jī)實(shí)體:狀態(tài)變更、心跳感知等
L3實(shí)體:IP段添加、刪除、IP分配、釋放等
本地存儲(chǔ)實(shí)體:存儲(chǔ)的占用與釋放

接下來看一下聚合中的對(duì)象,我們把及格聚合根識(shí)別出來:
物理機(jī)聚合的中的聚合根是物理機(jī)
網(wǎng)絡(luò)聚合中的聚合根是L2網(wǎng)絡(luò)
鏡像聚合中的聚合根是鏡像服務(wù)器
虛擬機(jī)聚合中的聚合根是虛擬機(jī)實(shí)體
而上面提到的實(shí)體屬性與方法我們已經(jīng)在圖中呈現(xiàn)出來了。
關(guān)于值對(duì)象,可以參考【ZStack】7.標(biāo)簽系統(tǒng)。該設(shè)計(jì)用于真實(shí)生產(chǎn)中。
3.3.4.2 設(shè)計(jì)代碼結(jié)構(gòu)
當(dāng)我們完成領(lǐng)域?qū)ο蟮姆治龊螅覀儽汩_始設(shè)計(jì)各領(lǐng)域?qū)ο笤诖a模型中的呈現(xiàn)方式了——即建立領(lǐng)域?qū)ο笈c代碼對(duì)象的映射關(guān)系。根據(jù)這種映射關(guān)系,服務(wù)人員可以快速定位到業(yè)務(wù)邏輯所在的代碼位置。
宏觀上,我們可以參考以下分層模型:

微觀實(shí)施上,我們可以參考COLA。
4.小結(jié)
本文和大家一起捋了一遍DDD,并在文里“憑空的”設(shè)計(jì)了一個(gè)項(xiàng)目。其實(shí)這個(gè)項(xiàng)目并非憑空,我參考了以前參與的開源項(xiàng)目ZStack并對(duì)它做出了簡化——該項(xiàng)目目前跑在大量的企業(yè)用戶的私有云中,迭代已有6年多。因此無論從設(shè)計(jì)還是落地來說,都有一定的參考經(jīng)驗(yàn)。
為了大家方便將文中的例子結(jié)合ZStack代碼理解,我這邊做了一個(gè)映射。

4.1 參考資料
關(guān)于ZStack的資料
【ZStack】4.進(jìn)程內(nèi)服務(wù)
【ZStack】7.標(biāo)簽系統(tǒng)
【ZStack】9.查詢API
ZStack源碼剖析:如何在百萬行代碼中快速迭代
ZStack源碼剖析之設(shè)計(jì)模式鑒賞——三駕馬車
ZStack Github Repo
雖然ZStack是個(gè)值得參考的項(xiàng)目,但其DDD的設(shè)計(jì)并不是特別明顯。因此在項(xiàng)目分層上也可以參考COLA
《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》
作者:泊浮目
鏈接:https://juejin.cn/post/6990937189643190302
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
