前言
? ? ? 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(Domain Drive Design)簡稱DDD,是2004年Eric Evans提出的一套軟件開發(fā)的方法論,隨著微服務(wù)的興起,這個(gè)概念又再次推到大眾眼前,熱度不減。
? ? ? 筆者有幸從17年接觸領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的理念,在實(shí)踐的過程中受益良多。對于DDD的概念每個(gè)人都會有自己的解釋,如果非要用自己的話給DDD下一個(gè)定義,我的理解是:DDD是一套指導(dǎo)我們需求分析,服務(wù)拆解和實(shí)際編碼落地的一整套完整地方法論,它提倡軟件模型和業(yè)務(wù)模型相關(guān)聯(lián),用代碼模擬客觀現(xiàn)實(shí)世界,以此做到客觀世界發(fā)生變化的同時(shí),代碼可以更好地隨之演變。
? ? ? 筆者是Java開發(fā)出身,所以在文章中舉例子使用的是java的語法和編程方式,請見諒,同時(shí)文章觀點(diǎn)若有偏差,還請指正。
0:靈魂拷問
1:軟件開發(fā)之前是否在分析完需求后第一件事就是新建數(shù)據(jù)庫表?
2:為什么咱們PO對象(數(shù)據(jù)庫持久化對象)需要寫那么多的get/set方法?
3:是否有一些時(shí)刻覺得service的代碼越寫越多,非?;靵y,往往改一個(gè)需求會牽一發(fā)而動(dòng)全身?
4:是否有想過,java還有很多語言都號稱是面向?qū)ο蟮恼Z言,但是我們好像依然是在面向過程編程?
5:我們平時(shí)談的微服務(wù)是什么,僅僅只是一個(gè)個(gè)拆分后的系統(tǒng)實(shí)例么
6:我們做微服務(wù)的時(shí)候,如何拆分是否有成型的方法論
如果您有以上疑問,請繼續(xù)往后,同時(shí)文章的末尾會對以上問題做解答。
1:領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)是什么
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DomainDriven Design,DDD)是EricEvans2004年提出的從系統(tǒng)分析到軟件建模的一套方法論。它要解決什么問題呢?就是將業(yè)務(wù)概念和業(yè)務(wù)規(guī)則轉(zhuǎn)換成軟件系統(tǒng)中概念和規(guī)則,從而降低或隱藏業(yè)務(wù)復(fù)雜性,使系統(tǒng)具有更好的擴(kuò)展性,以應(yīng)對復(fù)雜多變的現(xiàn)實(shí)業(yè)務(wù)問題。
? ? ? 是不是聽起來很誘人?并且你也會感覺到理所當(dāng)然,因?yàn)槲覀兙褪切枰@樣的一套體系。
? ? ? 現(xiàn)在我們在落地領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的時(shí)候,我們經(jīng)常會花很多的時(shí)間去討論微服務(wù),限界上下文怎么劃分,對象怎么建模,代碼怎么去分層和調(diào)用。
? ? ? 最初我覺得特別浪費(fèi)時(shí)間,但是過往的經(jīng)驗(yàn)又會告訴我們,很多細(xì)節(jié)上花更多的時(shí)間是值得的。
2:為什么用DDD

? ? ? 為什么用DDD這個(gè)問題,我先放兩張圖這里,按下不表,之后會再給大家分析一下這張圖可能大家印象會更加深刻一點(diǎn)。
? ? ? 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)并不是萬金油,他不能解決所有的問題,但是我們的代碼得有設(shè)計(jì),而最重要的也是設(shè)計(jì)本身,如果你現(xiàn)階段還是靠經(jīng)驗(yàn)去設(shè)計(jì)你的代碼,何不跟我們一起了解一下領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)。
? ? ? 其實(shí)在thoughtworks,盒馬,美團(tuán)點(diǎn)評,還有京東的7fresh都有做過DDD的實(shí)踐,盒馬的張群輝就說過,其實(shí)阿里不在乎你用的是什么設(shè)計(jì),可以是DDD也可以是別的,但是你得有設(shè)計(jì),而DDD就提供了這樣一套方法論。
? ? ? 其實(shí)說簡單一點(diǎn),領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)就是面向?qū)ο蟮倪M(jìn)階。
3:重塑我們的思維模式
? ? ? 面向?qū)ο蟠蠹叶荚偈煜げ贿^了,因?yàn)镴AVA就是面向?qū)ο蟮恼Z言。面向?qū)ο笞罨镜姆庋b,繼承,多態(tài)還有遵循著內(nèi)聚的原則,這些在領(lǐng)域驅(qū)動(dòng)都是成立的,雖然JAVA是面向?qū)ο蟮恼Z言,但是在實(shí)際開發(fā)的過程中,我們都是在面向過程去開發(fā),寫代碼的方式非常簡單從controller到service然后一直訪問到數(shù)據(jù)庫。當(dāng)我們提到領(lǐng)域驅(qū)動(dòng)的時(shí)候,一般會拿它和數(shù)據(jù)驅(qū)動(dòng)做一個(gè)對比?;貞浺幌挛覀?nèi)粘5拈_發(fā),我們都會先設(shè)計(jì)表,圍繞著這個(gè)表再去做業(yè)務(wù)邏輯的開發(fā)。
3.1:忘掉你的數(shù)據(jù)庫
? ? ? ? 現(xiàn)在我們做一個(gè)假設(shè),假設(shè)你的機(jī)器內(nèi)存無限大,永不斷電,永不宕機(jī),我們還需要數(shù)據(jù)庫么?答案是不需要,數(shù)據(jù)庫其實(shí)只是一個(gè)外部設(shè)備,他是我們在技術(shù)不夠發(fā)達(dá)的時(shí)候?qū)Τ志没囊环N妥協(xié)。我們設(shè)計(jì)模型的時(shí)候,過多的把“數(shù)據(jù)庫”這個(gè)不相干的因素帶進(jìn)來了這其實(shí)是不恰當(dāng)?shù)淖龇?,我們的模型需要的是持久化無關(guān)設(shè)計(jì)。對象模型對應(yīng)領(lǐng)域模型。接下來我們做一個(gè)直接的對比。

3.2:構(gòu)造具有業(yè)務(wù)含義的領(lǐng)域?qū)ο?br>
? ? ? 可能上圖的對比不太形象,我們可以結(jié)合代碼看一下,假設(shè)有一個(gè)需求,有一個(gè)狗,叫布魯,需要搖尾巴十次,如果只有g(shù)et和set方法可能寫出來的就是左邊的代碼,如果真正做到面向?qū)ο缶幊蹋敲蠢碚撋洗a應(yīng)該是右邊的樣子。

在起初學(xué)代碼的時(shí)候,我們new出來的狗和貓都是有屬性有動(dòng)作(方法)的,可是在WEB開發(fā)的過程中,我們寫出來的代碼漸漸地只有g(shù)et和set方法,讓對象僅僅成為了數(shù)據(jù)的載體,而不是一個(gè)具有行為和屬性活靈活現(xiàn)的對象,漸漸地我們仿佛遺失了什么。
再回頭看左邊的代碼,想象一下,它出現(xiàn)再我們的service層里的時(shí)候,一直沒有尾巴,沒有名字的狗被生了出來,然后憑空有生了只尾巴裝上去,取了個(gè)名字set進(jìn)去,然后被service層這個(gè)上帝之手將尾巴撥來撥去10次。是不是畫面就很驚悚的,我們的狗狗應(yīng)當(dāng)生而完整,也應(yīng)當(dāng)擁有對自己尾巴的控制權(quán),這才是真正符合客觀世界規(guī)律的代碼,才是真正的面向?qū)ο缶幊獭?/p>
3.2:貧血模型和充血模型
傳統(tǒng)的數(shù)據(jù)庫對象內(nèi)部只有單純的get/set方法,這樣的模型在領(lǐng)域驅(qū)動(dòng)里面成為貧血模型。get,set方法沒法反映出業(yè)務(wù)的邏輯。而充血模式下的對象才是行為飽滿的領(lǐng)域?qū)ο?,栩栩如生,溫婉?dòng)人。

如上圖對于一個(gè)用戶對象舉例,左邊是貧血模型構(gòu)造的對象,右邊是充血模型構(gòu)造的對象,我們可以簡單對比一下優(yōu)劣。
其實(shí)右圖這個(gè)對象依然不完善,因?yàn)閷τ趗ser來說,它的密碼應(yīng)該是不對外公開的,即使是密文的,也不應(yīng)該泄漏出去,所以password的get方法也是不可以對外部暴露的,關(guān)于密碼的修改和校驗(yàn)都應(yīng)該是這個(gè)對象內(nèi)部自己完成的。
貧血模型就像一個(gè)被service這個(gè)木偶繩擺弄的傀儡,是不健康的,貧血癥會引發(fā)失憶癥,在service里調(diào)用了幾十個(gè)set方法以后,我們自己往往都會忘了這一坨代碼是干什么的,但是在行為飽滿的領(lǐng)域?qū)ο笾?,每一個(gè)方法名明確地表明了這個(gè)方法的意圖,并且嚴(yán)格保證了對象的完整性和正確性。
不論是面向?qū)ο螅€是職責(zé)驅(qū)動(dòng)設(shè)計(jì),領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)在這里都是一致的,希望對象是自治的,其實(shí)領(lǐng)域驅(qū)動(dòng)并不是銀彈,并且一百個(gè)人寫出的領(lǐng)域驅(qū)動(dòng)代碼真的都是有一百種樣子,實(shí)踐的過程中也會有很多問題,還是那句話,不管是什么設(shè)計(jì),但是我們得有設(shè)計(jì)。
3.3:限界上下文拆分
是不是我們有了行為飽滿的對象,一切就萬事大吉了呢?不是的,請看下圖構(gòu)建的訂單對象。

我們可以看到這個(gè)充血模型設(shè)計(jì)的訂單對象也是有缺陷的,它包含了太多的語義,面向?qū)ο蟮脑O(shè)計(jì)有一個(gè)原則,叫做迪米特法則,又叫最小知識原則。圖中這個(gè)對象就成了上帝對象,godobject。如果有一個(gè)新的需求過來,需要在物流里支持自提,那么我們還得去改訂單,如果加上了一個(gè)分銷代理的錯(cuò)需求,我們還是在訂單上面修修補(bǔ)補(bǔ)。當(dāng)這種對象在我們系統(tǒng)里變多了以后,整個(gè)系統(tǒng)就會變成了一個(gè)大泥球,往往在新增需求以后,牽一發(fā)動(dòng)全身,該都不敢改,同時(shí)測試面也會很廣,很難覆蓋到所有的場景。
那么我們應(yīng)該怎么辦?拆!呢么怎么拆才合理呢?你說的不算,我說的也不算,我們需要協(xié)作完成,完成這個(gè)的方式就是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),在戰(zhàn)術(shù)戰(zhàn)略設(shè)計(jì)的環(huán)節(jié)提到的【事件風(fēng)暴】。事件風(fēng)暴是一種和領(lǐng)域?qū)<?,F(xiàn)E,UI,QA一起協(xié)作的一套需求分析和拆解的方法論,簡單而言是通過梳理用戶路徑對各個(gè)環(huán)節(jié)的統(tǒng)一語言以及事件進(jìn)行整理分析,提煉限界上下文從而做到微服務(wù)的拆分。感興趣的同學(xué)可以查閱相關(guān)的文章,后續(xù)咱們也會有分享。

3.4:統(tǒng)一語言
接下來就是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中最重要的一個(gè)概念,叫統(tǒng)一語言(UBIQUITOUS LANGUAGE),所有語言的流暢溝通需要我們統(tǒng)一語言和語境,代碼和現(xiàn)實(shí)世界之間亦如此。這樣我們需要做到業(yè)務(wù)模型和軟件模型相關(guān)聯(lián),同時(shí)相互提煉新的知識做補(bǔ)充,以此達(dá)到模型完善和擁抱變化。
3.5:六邊形架構(gòu)(游泳圈架構(gòu))
寫了太久的MVC三層架構(gòu),這種直截了當(dāng)?shù)淖龇ǖ拇_很有利于后續(xù)的迭代速度,但是這種模式并不能給我們徹底帶來不穩(wěn)定的隔離,數(shù)據(jù)庫DAO層的代碼充斥在service方法里,不明確的邊界帶來各種問題。DDD的戰(zhàn)術(shù)設(shè)計(jì)里提到了一種六邊形架構(gòu),也符合整潔架構(gòu)里提出的【游泳圈架構(gòu)】或者是【洋蔥架構(gòu)】,這是一張我奉為圭臬的架構(gòu)圖,后續(xù)會對整體的架構(gòu)做探討還有架構(gòu)的演進(jìn)做一些分析。

4:DDD學(xué)習(xí)門檻
如果你看到了這里,其實(shí)就已經(jīng)對DDD有了較大的興趣,本篇文章只是帶你們簡單了解了DDD的一些更加符合客觀現(xiàn)實(shí)的一些方向,還有很多內(nèi)容是沒有提到的。學(xué)習(xí)DDD需要克服內(nèi)心上的對很多專業(yè)術(shù)語的描述,不可否認(rèn),DDD提出了很多我們可能不太了解的概念,這些概念往往讓人望而卻步。光戰(zhàn)術(shù)設(shè)計(jì)(編碼落地)階段隨手羅列就會有一堆概念名詞。

但是那又怎樣呢,如果心里認(rèn)為它是對的,為什么不去實(shí)踐呢?高山仰止,景行行止,雖不能至,心向往之。
5:回到開始的問題

這張圖想表達(dá)的意思是,微服務(wù),DDD還有現(xiàn)如今很火的中臺戰(zhàn)略其實(shí)之間是有著非常相似的關(guān)系,他們都需要將業(yè)務(wù)邏輯沉淀,內(nèi)聚,將變化隔離到外面。
右邊的圖是一個(gè)隨著系統(tǒng)復(fù)雜度的提升,在代碼編寫難度上帶來提升的一個(gè)曲線圖,可以看到系統(tǒng)復(fù)雜度高的情況下,使用領(lǐng)域建模帶來的復(fù)雜程度提升比較穩(wěn)定,或許我們的項(xiàng)目是在一定的難度之下,但是又何妨我們?nèi)ψ约禾岢龈叩囊竽兀?/p>
其實(shí)看到這里,相信開始提出的幾個(gè)問題也就有答案了,至于為什么我們的代碼里充斥著get和set方法,大概是在原始的ORM框架里,必須通過get和set方法做數(shù)據(jù)的賦值和提取,亦或是開源的框架里大多是簡單API的操作,于是大家也就照貓畫虎挪用過來,具體原因也就不得而知了。
6:結(jié)語
這一次沒有展開提到DDD的一些概念和問題,只是簡單從我們?nèi)粘i_發(fā)的角度和DDD做了一下對比,之后也會寫相關(guān)的文檔去和大家一起探討和學(xué)習(xí)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),希望大家可以喜歡。