一.前言
ddd出現(xiàn)的意義在于從業(yè)務(wù)的角度而不是技術(shù)的角度去解決軟件的復(fù)雜性茂契,正如某位大師所言:“program is logic and control”,所有的程序本質(zhì)上就是兩件事:邏輯和控制,而邏輯則是決定了復(fù)雜性的下限望薄。舉個(gè)例子槽驶,商品交易的業(yè)務(wù)邏輯復(fù)雜性天然就勝過(guò)im聊天的業(yè)務(wù)邏輯,這種時(shí)候無(wú)論怎樣拆解蚯瞧,它的業(yè)務(wù)復(fù)雜性就擺在這里嘿期,而控制是我們盡可能用最優(yōu)的手段去實(shí)現(xiàn)我們的邏輯,這里有點(diǎn)類似于面向?qū)ο笾新窈希涌诤蛯?shí)現(xiàn)的感覺(jué)备徐,但又不局限于此。
對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)甚颂,我們接收到現(xiàn)實(shí)世界中的需求之后蜜猾,基本上是以下的套路來(lái)解決問(wèn)題:
1.確認(rèn)現(xiàn)實(shí)問(wèn)題秀菱;
2.將問(wèn)題映射到腦海中的概念模型;
3.用模型來(lái)解決問(wèn)題蹭睡;
4.編碼
我自身之前所有學(xué)到的具體的技術(shù)或者思想其實(shí)核心都是在做第三步和第四步衍菱,即如何用最優(yōu)的技術(shù)方案和最好的編碼方案,包括我們各種高大上的架構(gòu)肩豁,各種代碼上的tricky寫(xiě)法脊串,各種算法的應(yīng)用等等,這些本質(zhì)上都是在做實(shí)現(xiàn)層面的事情清钥。
但有的時(shí)候總會(huì)在想洪规,我們?cè)O(shè)計(jì)出來(lái)的模型真的能夠表達(dá)清楚我們的真實(shí)業(yè)務(wù)嗎?未來(lái)隨著業(yè)務(wù)的變更循捺,我們的底層模型和設(shè)計(jì)真的能夠支撐這種變更嗎斩例?
如果不能,那即便我的代碼設(shè)計(jì)多么優(yōu)雅合理从橘,可能也只是當(dāng)時(shí)的一時(shí)感動(dòng)罷了念赶,每一次的需求變更都依然可能會(huì)引發(fā)復(fù)雜的代碼變更,也給自己帶來(lái)了許多的叫苦不迭恰力。
所以叉谜,ddd的目的更偏向于解決第一步和第二步,即依據(jù)業(yè)務(wù)領(lǐng)域模型為核心來(lái)驅(qū)動(dòng)我們的系統(tǒng)設(shè)計(jì)踩萎。即抽象到更高層來(lái)理解的話停局,如何定義清楚問(wèn)題和模型,而后續(xù)的實(shí)現(xiàn)方案其實(shí)是由它來(lái)驅(qū)動(dòng)的香府。
正如上面所說(shuō)的董栽,問(wèn)題和模型決定了邏輯復(fù)雜性的下限,舉個(gè)例子企孩,之前在做商品的評(píng)價(jià)服務(wù)時(shí)锭碳,產(chǎn)品需求是這樣的:用戶只允許在訂單的流轉(zhuǎn)狀態(tài)中的某幾個(gè)狀態(tài)時(shí)(拼單成功,待發(fā)貨勿璃,待退款等等)發(fā)表評(píng)價(jià)擒抛,此時(shí)展示為待評(píng)價(jià)狀態(tài),而評(píng)價(jià)完成后則訂單扭轉(zhuǎn)為已評(píng)價(jià)狀態(tài)补疑。
這個(gè)需求乍一看歧沪,我們的第一反應(yīng)是在訂單的狀態(tài)機(jī)中添加兩個(gè)狀態(tài),待評(píng)價(jià)和已評(píng)價(jià)莲组,但后來(lái)會(huì)發(fā)現(xiàn)诊胞,無(wú)論怎樣推演,加入了評(píng)價(jià)的兩個(gè)狀態(tài)之后胁编,訂單的狀態(tài)機(jī)流轉(zhuǎn)都會(huì)變得非常復(fù)雜厢钧,因?yàn)闊o(wú)論是訂單的正向還是逆向流程中,都可能會(huì)有評(píng)論的狀態(tài)的加入和退出嬉橙。那我們退一步來(lái)思考早直,雖然產(chǎn)品需求是將訂單的原狀態(tài)列表中添加是否允許評(píng)價(jià)和是否已經(jīng)評(píng)價(jià)的狀態(tài),但我們仔細(xì)拆解就會(huì)發(fā)現(xiàn)市框,評(píng)價(jià)的狀態(tài)和訂單的狀態(tài)本質(zhì)上就是兩個(gè)事情霞扬。從核心領(lǐng)域的劃分上,也是兩個(gè)領(lǐng)域枫振,一個(gè)是訂單領(lǐng)域喻圃,一個(gè)是評(píng)輪領(lǐng)域,而獲取訂單的評(píng)論狀態(tài)本質(zhì)上是屬于評(píng)論領(lǐng)域而非訂單領(lǐng)域的工作粪滤。這樣事情一下子變得簡(jiǎn)單了斧拍,一個(gè)訂單能否被評(píng)價(jià)以及是否已經(jīng)被評(píng)價(jià)過(guò)了,完全交由評(píng)論領(lǐng)域來(lái)判斷杖小,訂單無(wú)需也不該維護(hù)和添加這么一個(gè)狀態(tài)肆汹。
所以使用ddd,是為了很好地解決領(lǐng)域模型到設(shè)計(jì)模型的同步予权、演化昂勉,最后再將反映了領(lǐng)域的設(shè)計(jì)模型轉(zhuǎn)為實(shí)際的代碼。
二.ddd的基本概念
官方的解釋是這樣的:領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(Domain-driven design扫腺,縮寫(xiě) DDD)是一種通過(guò)將實(shí)現(xiàn)連接到持續(xù)進(jìn)化的模型來(lái)滿足復(fù)雜需求的軟件開(kāi)發(fā)方法岗照。這個(gè)說(shuō)起來(lái)實(shí)在是有點(diǎn)太虛無(wú)縹緲了,還記得上文中提到的解決問(wèn)題的四個(gè)步驟笆环,那現(xiàn)在如果用ddd的思路來(lái)指導(dǎo)攒至,應(yīng)該是怎樣的呢?
1.確認(rèn)現(xiàn)實(shí)問(wèn)題躁劣;---和領(lǐng)域?qū)<遥óa(chǎn)品經(jīng)理等)一起嗓袱,通過(guò)多次的拆解,交流和溝通习绢,構(gòu)建一套整個(gè)項(xiàng)目統(tǒng)一的領(lǐng)域語(yǔ)言渠抹,并用這些領(lǐng)域語(yǔ)言定義清楚具體的原型,操作和邏輯闪萄。
2.將問(wèn)題映射到腦海中的概念模型梧却;----基于上述的統(tǒng)一語(yǔ)言,構(gòu)建出ddd中的領(lǐng)域模型败去,包括領(lǐng)域?qū)嶓w放航,上下文的邊界,領(lǐng)域事件圆裕,聚合等等广鳍;
3.用模型來(lái)解決問(wèn)題荆几;----在ddd的思路下劃分微服務(wù),設(shè)計(jì)包結(jié)構(gòu)赊时,分層設(shè)計(jì)吨铸,定義接口,定義聚合根等等
4.編碼祖秒;----編碼實(shí)現(xiàn)
三.用ddd的思路做方案設(shè)計(jì)
以我最近一段時(shí)間做的津貼系統(tǒng)為例诞吱,來(lái)看下如何做具體的方案設(shè)計(jì)與拆解;
1.首先竭缝,明確產(chǎn)品需求和用例:
在用戶側(cè)房维,分三個(gè)操作:
收入:要能夠做到津貼能夠以多種形式和渠道來(lái)發(fā)放和領(lǐng)取,同時(shí)要保證每一筆津貼收入都有有效期的概念(包括開(kāi)始生效和結(jié)束生效)抬纸,同時(shí)要求這個(gè)時(shí)間必須十分精準(zhǔn)咙俩,比如在大促時(shí),要求大促期間生效的津貼必須在定點(diǎn)失效和生效湿故。
支出:要求支出的時(shí)候可以合并支出暴浦,并且能夠按照有效期快失效的先支出;
退還:要求支持退還的操作晓锻,但已經(jīng)失效的則不再退還歌焦;
在平臺(tái)側(cè):
創(chuàng)建津貼:制定特定規(guī)則的津貼,比如有效期等砚哆;
發(fā)放津貼:通過(guò)各種各樣的形式發(fā)放給用戶独撇;
2.確認(rèn)各個(gè)業(yè)務(wù)核心領(lǐng)域的邊界劃分:
首先我們可以發(fā)現(xiàn),津貼的領(lǐng)取其實(shí)是由上層系統(tǒng)所驅(qū)動(dòng)的躁锁,最終經(jīng)過(guò)一定的規(guī)則之后發(fā)放到用戶的個(gè)人津貼賬戶上纷铣,通過(guò)大量的討論和梳理之后,我們會(huì)在這里發(fā)生第一次的業(yè)務(wù)領(lǐng)域劃分战转,我們將領(lǐng)取劃分為了四個(gè)子業(yè)務(wù)領(lǐng)域:
1.上層的發(fā)放渠道搜立,比如紅包領(lǐng)域,任務(wù)領(lǐng)域等等槐秧,這里負(fù)責(zé)計(jì)算用戶是否滿足了某些條件啄踊,然后可以自由組合并決定給用戶的發(fā)放金額。
2.津貼控制領(lǐng)域:這里負(fù)責(zé)津貼規(guī)則的制定刁标,以及如何發(fā)放颠通,比如津貼的有效期,津貼的領(lǐng)取個(gè)數(shù)限制膀懈,防重入的控制等等(由于時(shí)間關(guān)系顿锰,一部分限額限次數(shù)等邏輯目前移交給上層渠道做了,但應(yīng)該有這么個(gè)領(lǐng)域存在);
3.個(gè)人津貼賬戶領(lǐng)域:這里是最終發(fā)放到的個(gè)人賬戶上硼控,負(fù)責(zé)管理用戶的津貼賬戶刘陶,包括收入,支出和退還等等牢撼;
4.支付與核銷領(lǐng)域:負(fù)責(zé)用戶真正使用津貼支付的邏輯匙隔,比如與財(cái)務(wù)的核銷,平臺(tái)側(cè)資金的支出與商戶的資金收入等等浪默;
這里核心領(lǐng)域劃分清楚之后牡直,其實(shí)就有點(diǎn)映射到了我們微服務(wù)的劃分缀匕,也對(duì)應(yīng)到了人員的劃分纳决,這時(shí)候任務(wù)就開(kāi)始拆解了,我們會(huì)發(fā)現(xiàn)其實(shí)上層的發(fā)放渠道與津貼其實(shí)是基本無(wú)關(guān)的乡小,那就從我們的核心領(lǐng)域移除了出去阔加,我們目前重點(diǎn)關(guān)心了一個(gè)領(lǐng)域,個(gè)人賬戶满钟;
以個(gè)人賬戶領(lǐng)域?yàn)槔だ疲俏覀兊臉I(yè)務(wù)領(lǐng)域核心目前就比較聚合了,只關(guān)注津貼的入賬和出帳湃番,以及內(nèi)部的生效和失效夭织,其他的都無(wú)需關(guān)注。
3.確認(rèn)核心領(lǐng)域中的模型與邊界上下文:
1.為了滿足產(chǎn)品設(shè)計(jì)中津貼支出時(shí)能夠一次性的合并支付吠撮,因此我們定義了用戶總賬戶的概念尊惰,即每個(gè)用戶用擁有一個(gè)個(gè)人的總賬戶,支持在下單時(shí)扣減這個(gè)總額泥兰,所以是津貼實(shí)體匯總在一起之后有個(gè)總賬戶弄屡;
2.為了滿足有效性的需求(尤其是準(zhǔn)時(shí)準(zhǔn)點(diǎn)的生效失效),我們將每個(gè)用戶總帳戶又分為三個(gè)部分鞋诗,預(yù)生效(當(dāng)晚23:59:59秒開(kāi)始生效)膀捷,預(yù)失效(當(dāng)晚23:59:59秒開(kāi)始失效),普通(生效期內(nèi)可直接扣減)削彬,這樣即使更新腳本不及時(shí)全庸,也可以通過(guò)這三個(gè)子賬戶決定可以支出的總金額;
3.為了滿足退還有效性的需求融痛,我們定義了津貼實(shí)體和支付訂單的概念糕篇,用來(lái)標(biāo)示每一筆聚合的支出包含了哪些具體的津貼實(shí)體的支出,用來(lái)做將來(lái)的退還酌心。
4.為了保障數(shù)據(jù)的可追溯和可恢復(fù)拌消,我們定義了用戶流水的概念,包括收入流水,支出流水墩崩,退款流水氓英,更新流水等。
所以在我們的模型設(shè)計(jì)中鹦筹,通過(guò)流水可以重放出每一筆津貼實(shí)體铝阐,每一筆津貼實(shí)體聚合起來(lái)可以導(dǎo)出用戶總額。
而在這個(gè)領(lǐng)域下铐拐,所有的模型定義都是在我們這個(gè)邊界下是明確且獨(dú)立的徘键,比如說(shuō)對(duì)于上層的紅包系統(tǒng)來(lái)說(shuō),它所謂的發(fā)"一筆津貼"遍蟋,實(shí)質(zhì)上是對(duì)應(yīng)我們這里的津貼實(shí)體吹害。對(duì)于上層的交易系統(tǒng)來(lái)說(shuō),它所謂的支付"一筆津貼"虚青,對(duì)應(yīng)的其實(shí)是我們的總額它呀。所以即便是同一個(gè)名詞,在不同的核心領(lǐng)域下棒厘,也是映射著不同的含義纵穿。
接下來(lái),我們看下如何ddd如何指導(dǎo)我們做具體的代碼編寫(xiě)
四.用ddd的思路編寫(xiě)代碼
1.分層設(shè)計(jì):
如下圖所示:
如果我們只是簡(jiǎn)單的增刪改查奢人,其實(shí)是不需要這么麻煩的谓媒,甚至我們完全可以不需要這么多層,只需要把業(yè)務(wù)邏輯退化為一個(gè)sql腳本就可以支持了何乎,但如果業(yè)務(wù)邏輯比較復(fù)雜的情況下句惯,我們一定必不可少的要做的事情就是關(guān)注點(diǎn)的分離,但在分離的同時(shí)也要保證內(nèi)部的交互關(guān)系宪赶。這其中宗弯,領(lǐng)域?qū)邮钦麄€(gè)模型的精髓,
那在golang里面是怎樣的呢搂妻?目前我們的分包設(shè)計(jì)是這樣的:
---ao:負(fù)責(zé)編排do和對(duì)外界的協(xié)議防腐層
---infra:基礎(chǔ)設(shè)施層蒙保,
? ? --dao:數(shù)據(jù)庫(kù)層,封裝了基本的數(shù)據(jù)庫(kù)實(shí)現(xiàn)欲主,比如各個(gè)表的增刪改查邓厕,數(shù)據(jù)庫(kù)的事務(wù)實(shí)現(xiàn)等等;
? ? --integration:第三方接口扁瓢,包括rpc接口详恼,mq的producer等;
---do:核心領(lǐng)域?qū)?/p>
---impl:核心領(lǐng)域?qū)訉?shí)現(xiàn)
? ? beanfatory:impl下的文件引几,負(fù)責(zé)所有的實(shí)例化昧互,包括單個(gè)do的build,以及一些單例的dao和service的構(gòu)建
---service:對(duì)外接口層,包括rpc接口和mq的consumer等等敞掘;
2.領(lǐng)域?qū)釉O(shè)計(jì):
正如我們前面所說(shuō)叽掘,一切以領(lǐng)域驅(qū)動(dòng),那我們編寫(xiě)代碼的時(shí)候也是玖雁,先想清楚自己的系統(tǒng)功能以及邊界更扁,那第一步是設(shè)計(jì)對(duì)外的協(xié)議,第二步就是定義我們的領(lǐng)域?qū)幽P停?/p>
目前我這邊把領(lǐng)域?qū)拥膁o分為了三類:
a: 實(shí)體類do:
這類模型是擁有真實(shí)的實(shí)體赫冬,能夠被定義成實(shí)體的模型有一個(gè)最大的特點(diǎn):具有唯一標(biāo)識(shí)性浓镜。即在我們當(dāng)前的系統(tǒng)上下文內(nèi),它需要能夠通過(guò)這個(gè)唯一標(biāo)識(shí)被追溯到劲厌。通常這類實(shí)體還會(huì)有一些自身的方法膛薛,與貧血模型相對(duì)的,我們更希望實(shí)體自身能夠表達(dá)自己的屬性和動(dòng)作脊僚,而不是把自己的領(lǐng)域邏輯散落在其他地方相叁;
b:聚合類do:
有很多情況下遵绰,是需要將實(shí)體和值對(duì)象們組合到一起做服務(wù)的辽幌,設(shè)計(jì)聚合的最大原則在于,要滿足聚合的一致性邊界椿访;
舉個(gè)例子乌企,我們系統(tǒng)對(duì)外要提供用戶的收入服務(wù),那這個(gè)核心的領(lǐng)域模型就需要針對(duì)一筆入賬成玫,要做三件事情加酵,第一,插入一筆收入流水哭当,第二:插入一條津貼實(shí)體猪腕;第三,更新用戶的總額度钦勘。
這個(gè)聚合的一致性原則在于:
這三件事情陋葡,要么同時(shí)發(fā)生,要么直接失敵共伞腐缤;其實(shí)就是一個(gè)事務(wù)操作
那很明顯,在這種情況下肛响,單一的實(shí)體總額度或者津貼實(shí)體都不能直接在自己的實(shí)體方法中執(zhí)行修改操作岭粤,因?yàn)檫@樣會(huì)造成整體的不一致,那這種時(shí)候特笋,我們是這樣處理的:
首先是三個(gè)實(shí)體:
type AllowanceEntity{
EnttyId uint64
UID uint64
Amount uint64
xxxx...
}
type AllowanceIncomeRecord{
RecordId uint64
UID uint64
Amount uint64
xxxx...
}
tyoe TotalAmountEntity{
UID uint64
Amount uint64
}
然后是聚合:
type IncomeAggregate struct{
TotalAmountEntity TotalAmountEntity
allowanceIncomeRecord AllowanceIncomeRecord
allowanceEntity AllowanceEntity
}
注意剃浇,這里很明顯,這個(gè)聚合,外界能夠訪問(wèn)到它的聚合根也就是總額度虎囚,但是無(wú)法訪問(wèn)到其他的對(duì)象臼寄,因?yàn)槠渌麑?duì)象可以看作是它的私有屬性,不對(duì)外暴露溜宽,這一點(diǎn)我覺(jué)得golang通過(guò)大小寫(xiě)區(qū)分有點(diǎn)容易分不清吉拳。
然后對(duì)于聚合來(lái)說(shuō),聚合根是可以被外界訪問(wèn)到的,所以如果從封裝的角度出發(fā)适揉,按照golang的思路這里其實(shí)應(yīng)該作為一個(gè)子包存在留攒,但這樣總覺(jué)得有點(diǎn)太細(xì)粒度,所以還在探索中嫉嘀。
最后炼邀,津貼總額提供一個(gè)cqs查詢方法,將寫(xiě)操作轉(zhuǎn)換為一個(gè)讀操作剪侮,保證了實(shí)體沒(méi)有被修改拭宁,我自己實(shí)踐后認(rèn)為,這樣的好處在于瓣俯,在項(xiàng)目里有一大堆代碼的時(shí)候杰标,我可以很清楚的知道,只要是有返回值的方法彩匕,就一定不會(huì)對(duì)原來(lái)的實(shí)例有任何的修改腔剂,這樣可以很好的節(jié)省我們的思考成本。
func (s*TotalAmountEntity) afterIncome(ctx context.context,entity AllowanceEntity) TotalAmountEntity{
copy:=s.copy()
copy.amount+=entity.amount;
return copy
}
津貼實(shí)體提供一個(gè)init方法:
津貼記錄提供一個(gè)init方法:
最終在聚合中是一個(gè)類似如下的函數(shù):
func(s*Aggregate) Income(ctx context.context,params xxxx){
//創(chuàng)建流水
s.record:=initRecord()
//創(chuàng)建津貼實(shí)體
s.entity:=initEntity()
//更新總額度
s.total=s.total.afterIncome(ctx,s.entity)
//持久化聚合內(nèi)的全部實(shí)體
persist(s.record,s.entity,s.total)
}
那這就是一個(gè)聚合驼仪,聚合的最大特點(diǎn)在于掸犬,在聚合的邊界內(nèi)保證數(shù)據(jù)的一致性,也就是說(shuō)绪爸,在上面那個(gè)income方法里湾碎,這三者的變更是一致的,不存在產(chǎn)生了流水奠货,卻沒(méi)有插入津貼實(shí)體這種事情存在介褥。
c:service方法
其實(shí)就是有點(diǎn)像膠水代碼,本身并沒(méi)有太多的復(fù)雜業(yè)務(wù)邏輯仇味,只是用來(lái)做一些do或者聚合的生成以及使用呻顽,這類方法都是無(wú)狀態(tài)的。
3.基礎(chǔ)設(shè)施層:
這一層其實(shí)是基礎(chǔ)設(shè)施的封裝丹墨,這一層是與我們的領(lǐng)域模型無(wú)關(guān)的廊遍,只是單純的基礎(chǔ)設(shè)施,比如一個(gè)xxxDAO,那它的實(shí)現(xiàn)可以是單表的mysql贩挣,也可以是分庫(kù)分表后的mysql喉前,也可以是redis緩存没酣,又或者是local cache,所以這里是完全與業(yè)務(wù)無(wú)關(guān)的卵迂,所以我這邊的實(shí)現(xiàn)方案是設(shè)計(jì)成接口裕便,然后讓領(lǐng)域?qū)右蕾囘@個(gè)接口,但不依賴具體的實(shí)現(xiàn)见咒,實(shí)現(xiàn)可以做自由的更改和替換偿衰。
五.用ddd的思路反哺
這一點(diǎn)是我最近一段時(shí)間慢慢感受到的,在和產(chǎn)品討論需求時(shí)改览,其實(shí)很多時(shí)候也是一種大家共同摸索共同討論的狀態(tài)下翎,因此一個(gè)好的方法論其實(shí)不只是可以幫助到自身,也可以反哺到團(tuán)隊(duì)中的其他同學(xué)宝当,同樣的视事,在面臨產(chǎn)品需求時(shí)超营,用ddd的思路做產(chǎn)品的需求定義和模塊拆解渔期,這種反而會(huì)比單純的實(shí)現(xiàn)更考驗(yàn)功力。