DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)如何進(jìn)行工程化落地
引言
前面幾篇文章中,筆者給大家闡述了DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的三大過(guò)程,重點(diǎn)圍繞如何通過(guò)戰(zhàn)略設(shè)計(jì)與戰(zhàn)術(shù)設(shè)計(jì)進(jìn)行DDD領(lǐng)域模型分析以及沉淀,但是還沒(méi)有涉及到工程層面的落地。所有的這些架構(gòu)理論或者設(shè)計(jì)模式到最后都是為了讓我們的代碼結(jié)構(gòu)更加清晰,擴(kuò)展性以及維護(hù)性更強(qiáng)。從而開(kāi)發(fā)出bug少穩(wěn)定性更好的應(yīng)用。因此本文重點(diǎn)介紹如何進(jìn)行DDD工程化落地。????
DDD領(lǐng)域分層
當(dāng)我們完成邊界上下文的劃分以及領(lǐng)域模型的構(gòu)建之后,就需要進(jìn)行微服務(wù)的工程結(jié)構(gòu)設(shè)計(jì)了。在進(jìn)行工程結(jié)構(gòu)落地之前,我們需要先確定微服務(wù)內(nèi)部的領(lǐng)域分層結(jié)構(gòu)。首先我們要思考一個(gè)問(wèn)題,為什么要進(jìn)行領(lǐng)域分層呢?實(shí)際上領(lǐng)域分層就是一種分而治之的思想,主要為了避免將代碼工程開(kāi)發(fā)成一坨大泥球,各種業(yè)務(wù)復(fù)雜邏輯以及技術(shù)細(xì)節(jié)都糅合在一起,導(dǎo)致工程后期難以維護(hù)同時(shí)也會(huì)削弱領(lǐng)域模型的完整性。另外通過(guò)領(lǐng)域分層設(shè)計(jì),更加容易開(kāi)發(fā)出高內(nèi)聚低耦合的軟件服務(wù),在模塊復(fù)用以及擴(kuò)展性方面也會(huì)有更好的表現(xiàn)。
搞清楚為什么進(jìn)行領(lǐng)域分層之后,我們來(lái)確定下如何進(jìn)行微服務(wù)內(nèi)部的領(lǐng)域分層,因?yàn)榉謱釉O(shè)計(jì)的好壞直接決定了我們微服務(wù)的工程結(jié)構(gòu)合理性以及后期團(tuán)隊(duì)落地的效果。不過(guò)遺憾的是,真正的領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)在怎么規(guī)范工程結(jié)構(gòu)上面實(shí)際也沒(méi)有非常明確具體的規(guī)范,因此我們需要根據(jù)自己的實(shí)踐經(jīng)驗(yàn)以及思考和理解來(lái)進(jìn)行劃分設(shè)計(jì)。下圖中左邊的分層方式是 Eric Evans在《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》中提出的,但是這種分層方式實(shí)際上是存在明顯不足的。為什么這么說(shuō)呢?
大家都比較熟悉MVC的開(kāi)發(fā)方式,因此在團(tuán)隊(duì)中進(jìn)行DDD落地的時(shí)候,很多同學(xué)有疑問(wèn)為什么要讓基礎(chǔ)設(shè)施層反向依賴領(lǐng)域?qū)幽兀蠹叶加X(jué)得很別扭。按照正常邏輯來(lái)說(shuō),領(lǐng)域模型發(fā)生變化后需要進(jìn)行持久化保存,很明顯是領(lǐng)域?qū)右蕾嚮A(chǔ)設(shè)施層,但是在工程落地的時(shí)候還是基礎(chǔ)設(shè)施層依賴領(lǐng)域?qū)樱@是為什么呢?實(shí)際上無(wú)論是什么樣的架構(gòu)都遵循這樣的設(shè)計(jì)原則,我們都認(rèn)為業(yè)務(wù)領(lǐng)域是核心域,核心域?qū)ν獠康囊蕾囋缴僭胶茫虼诵枰獙?shí)現(xiàn)將技術(shù)復(fù)雜度與業(yè)務(wù)復(fù)雜度相分離。那么在 基于DDD的架構(gòu)中,領(lǐng)域?qū)泳褪呛诵膶右虼怂膶?duì)外依賴越少越好,也就是說(shuō)應(yīng)該是非核心依賴核心而不是核心依賴非核心。
在我們以往的開(kāi)發(fā)模式中,一般都是service接口去調(diào)用dao接口進(jìn)行相關(guān)的數(shù)據(jù)操作,但是我們發(fā)現(xiàn)一旦我們進(jìn)行一些優(yōu)化操作,比如增加緩存來(lái)提升數(shù)據(jù)查詢的效率,我們就需要修改service層的代碼,但是實(shí)際上增加緩存屬于技術(shù)實(shí)現(xiàn)細(xì)節(jié),并不在業(yè)務(wù)范疇之內(nèi),可實(shí)際情況就是技術(shù)細(xì)節(jié)有變化就會(huì)影響到業(yè)務(wù)層,因此這樣的狀況明顯是不合理的。
因此上圖中優(yōu)化后的依賴倒置,表面上是基礎(chǔ)設(shè)施層依賴領(lǐng)域?qū)樱浔举|(zhì)是技術(shù)實(shí)現(xiàn)細(xì)節(jié)依賴于接口抽象,這是一種編程思想的轉(zhuǎn)變。將repo層的接口定義在domain層,具體實(shí)現(xiàn)細(xì)節(jié)由基礎(chǔ)實(shí)施層去完成,這樣實(shí)現(xiàn)了對(duì)于技術(shù)實(shí)現(xiàn)細(xì)節(jié)的解耦。同時(shí)不僅保證了domain層模型的穩(wěn)定性,也提升了基礎(chǔ)設(shè)施層實(shí)現(xiàn)的靈活性。
各層模型數(shù)據(jù)對(duì)象
在介紹各層對(duì)象之前,我們先思考一個(gè)問(wèn)題。為什么每一層都要有不同的數(shù)據(jù)模型對(duì)象呢?不同分層在進(jìn)行接口調(diào)用的時(shí)候,每次都要進(jìn)行模型對(duì)象轉(zhuǎn)換,很多時(shí)候?qū)ο笾械膮?shù)還都是一樣的,這樣做不是多此一舉嗎?這也是我在團(tuán)隊(duì)中推行DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)落地的時(shí)候,很多同學(xué)提出來(lái)的疑問(wèn)。
但是大家有沒(méi)有想過(guò)一個(gè)問(wèn)題,假設(shè)我們使用一個(gè)模型數(shù)據(jù)對(duì)象來(lái)串接代碼中的各個(gè)分層,如果哪一天數(shù)據(jù)庫(kù)表字段增加了或者修改了,那么這個(gè)變化會(huì)在各個(gè)分層中蔓延開(kāi)來(lái),這樣即使做了應(yīng)用分層但是實(shí)際上和一個(gè)大泥球的應(yīng)用沒(méi)有什么本質(zhì)區(qū)別,另外對(duì)于核心的領(lǐng)域?qū)觼?lái)說(shuō)也需要屏蔽底層細(xì)節(jié)變化對(duì)于領(lǐng)域模型的影響,避免領(lǐng)域模型穩(wěn)定性問(wèn)題。因此為了避免上述問(wèn)題的發(fā)生,各個(gè)分層應(yīng)該都有數(shù)據(jù)自己的模型數(shù)據(jù)對(duì)象,各司其職。
VO(View Object,視圖對(duì)象):該層的視圖數(shù)據(jù)對(duì)象主要的作用就是將應(yīng)用層的數(shù)據(jù)進(jìn)行組裝后形成用于頁(yè)面展示的數(shù)據(jù)。
DTO(Data Transfer Object,數(shù)據(jù)傳輸對(duì)象):DTO主要作為Application層的入?yún)⒑统鰠ⅲ糜谟脩艚涌趯优c應(yīng)用層之間的數(shù)據(jù)傳輸。比如接口參數(shù)中的Command、Query以及事件Event,以及Request、Response等都屬于DTO的范疇。DTO的價(jià)值在于適配不同的業(yè)務(wù)場(chǎng)景的入?yún)⒑统鰠ⅲ苊庾寴I(yè)務(wù)對(duì)象變成一個(gè)萬(wàn)能大對(duì)象。
Model(領(lǐng)域?qū)ο螅?/strong>:領(lǐng)域?qū)ο笫俏覀兂Uf(shuō)的核心的領(lǐng)域模型對(duì)象,它的字段和方法應(yīng)該具備強(qiáng)烈的業(yè)務(wù)語(yǔ)義,和持久化方式無(wú)關(guān)。也就是說(shuō),Entity和PO很可能有著完全不一樣的字段命名和字段類(lèi)型,甚至嵌套關(guān)系。Entity的生命周期應(yīng)該僅存在于內(nèi)存中,不需要可序列化和可持久化。
PO (Persistent Objec,持久化對(duì)象):實(shí)際上是我們?cè)谌粘9ぷ髦凶畛R?jiàn)的數(shù)據(jù)模型。但是在DDD的規(guī)范里,PO應(yīng)該僅僅作為數(shù)據(jù)庫(kù)物理表格的映射,不能參與到業(yè)務(wù)邏輯中。為了簡(jiǎn)單明了,PO的字段類(lèi)型和名稱(chēng)應(yīng)該和數(shù)據(jù)庫(kù)物理表格的字段類(lèi)型和名稱(chēng)一一對(duì)應(yīng),這樣我們不需要去跑到數(shù)據(jù)庫(kù)上去查一個(gè)字段的類(lèi)型和名稱(chēng)。
各層數(shù)據(jù)流轉(zhuǎn)
上文中分別說(shuō)到了領(lǐng)域分層結(jié)構(gòu)以及各個(gè)數(shù)據(jù)對(duì)象的不同含義和用途,那么我們接下來(lái)就看下各個(gè)數(shù)據(jù)對(duì)象在DDD的各個(gè)領(lǐng)域分層中是怎么進(jìn)行數(shù)據(jù)流轉(zhuǎn)的吧。
在用戶接口層,它需要接收來(lái)自WEB端、APP端以及其他的外部數(shù)據(jù)請(qǐng)求,并將請(qǐng)求通過(guò)DTO向應(yīng)用層進(jìn)行傳遞,根據(jù)應(yīng)用層返回的DTO數(shù)據(jù),再將DTO轉(zhuǎn)化為頁(yè)面需要呈現(xiàn)的VO數(shù)據(jù)。
我們通過(guò)Query對(duì)象表示查詢,用Command對(duì)象表示數(shù)據(jù)操作。當(dāng)請(qǐng)求到達(dá)應(yīng)用層后,如果需要調(diào)用外部服務(wù)的接口,那么我們需要通過(guò)應(yīng)用層的防腐層進(jìn)行調(diào)用。為什么需要防腐層呢?主要就是為了隔離變化,防止外在服務(wù)的數(shù)據(jù)變化影響應(yīng)用層的代碼,如果真的需要修改那么直接在防腐層中進(jìn)行修改就好。
在領(lǐng)域?qū)樱彝ǔJ褂玫氖莔odel,可以理解為業(yè)務(wù)領(lǐng)域模型,主要包括實(shí)體以及值對(duì)象。在應(yīng)用層會(huì)將model作為參數(shù)進(jìn)行領(lǐng)域?qū)咏涌诘恼{(diào)用完成核心的業(yè)務(wù)邏輯。在一些其他的書(shū)中,很多人喜歡使用DO來(lái)作為領(lǐng)域?qū)拥臄?shù)據(jù)承載對(duì)象,但是我個(gè)人還是覺(jué)得model更適合,因?yàn)閺拿Q(chēng)上面更好理解一點(diǎn),更加直觀一點(diǎn)。
領(lǐng)域?qū)又邪藗}(cāng)儲(chǔ)的接口,具體的實(shí)現(xiàn)在基礎(chǔ)設(shè)施層中,這是一種依賴倒置的設(shè)計(jì)方式,實(shí)現(xiàn)領(lǐng)域?qū)优c基礎(chǔ)層的解耦。大致的數(shù)據(jù)轉(zhuǎn)化流向如下圖所示。
工程結(jié)構(gòu)落地
在確定好領(lǐng)域分層各層的依賴關(guān)系之后,我們需要設(shè)計(jì)下具體可落地的工程結(jié)構(gòu),如下圖所示。
starter層:該層屬于用戶接口層,服務(wù)的啟動(dòng)類(lèi)也在該層,主要負(fù)責(zé)服務(wù)的啟動(dòng)以及對(duì)外提供REST接口或者RPC接口。
business層:主要負(fù)責(zé)業(yè)務(wù)邏輯的編排,不負(fù)責(zé)具體的業(yè)務(wù)邏輯,因此該層應(yīng)該是比較薄的。
integration層:ACL層,即防腐層,主要與外部服務(wù)接口進(jìn)行交互,它的存在主要為了將微服務(wù)本身的業(yè)務(wù)模型與外部服務(wù)的模型進(jìn)行隔離,避免外部服務(wù)模型的變化影響到自身服務(wù)領(lǐng)域模型的穩(wěn)定性。
domain層:領(lǐng)域?qū)訉儆诤诵膶樱械臉I(yè)務(wù)領(lǐng)域模型以及領(lǐng)域服務(wù)都在該層,沉淀了整個(gè)業(yè)務(wù)域中的業(yè)務(wù)領(lǐng)域模型,也就說(shuō)核心的業(yè)務(wù)邏輯都落在此層,同時(shí)定義了repository層的接口。
common層:通用層,主要放一些支撐其他業(yè)務(wù)的代碼,比如各種工具類(lèi),各種常量定義、錯(cuò)誤碼定義以及多語(yǔ)言等。
repository層:屬于基礎(chǔ)設(shè)施層,主要負(fù)責(zé)與數(shù)據(jù)庫(kù)、Redis等進(jìn)行交互,實(shí)現(xiàn)領(lǐng)域?qū)佣x的接口。
總結(jié)
本文主要和大家聊了怎樣進(jìn)行DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的落地,分析了為什么要進(jìn)行領(lǐng)域分層以及為什么要實(shí)現(xiàn)依賴倒轉(zhuǎn)的領(lǐng)域分層結(jié)構(gòu),同時(shí)基于依賴倒轉(zhuǎn)的領(lǐng)域分層結(jié)構(gòu)設(shè)計(jì)了可落地的微服務(wù)工程結(jié)構(gòu)。希望通過(guò)本文可以為大家在落地DDD的時(shí)候提供一點(diǎn)工程結(jié)構(gòu)設(shè)計(jì)的思路。后面的文章將從代碼層面入手和大家分享下如何通過(guò)代碼實(shí)現(xiàn)DDD落地。