DDD 模式從天書(shū)到實(shí)踐

背景

正所謂有人的地方就有江湖,有設(shè)計(jì)的地方也一定會(huì)有架構(gòu)。如果你是一位軟件行業(yè)的老鳥(niǎo),你一定會(huì)有這樣的經(jīng)歷:一個(gè)業(yè)務(wù)的初期,普通的 CRUD 就能滿(mǎn)足,業(yè)務(wù)線也很短,此時(shí)系統(tǒng)的一切都看起來(lái)很 nice,但隨著迭代的不斷演化,以及業(yè)務(wù)邏輯越來(lái)越復(fù)雜,我們的系統(tǒng)也越來(lái)越冗雜,模塊彼此關(guān)聯(lián),甚至沒(méi)有人能描述清楚每個(gè)細(xì)節(jié)。當(dāng)新需求需要修改一個(gè)功能時(shí),往往光回顧該功能涉及的流程就需要很長(zhǎng)時(shí)間,更別提修改帶來(lái)的不可預(yù)知的影響面。于是 RD 就加開(kāi)關(guān),小心翼翼地切流量上線,一有問(wèn)題趕緊關(guān)閉開(kāi)關(guān)。

面對(duì)此般場(chǎng)景,你要么跑路,要么重構(gòu)。重構(gòu)是克服演進(jìn)式設(shè)計(jì)中大雜燴問(wèn)題的主力,通過(guò)在單獨(dú)的類(lèi)及方法級(jí)別上做一系列小步重構(gòu)來(lái)完成,我們可以很容易重構(gòu)出一個(gè)獨(dú)立的類(lèi)來(lái)放某些通用的邏輯,但是,你會(huì)發(fā)現(xiàn)你很難給它一個(gè)業(yè)務(wù)上的含義,只能給予一個(gè)技術(shù)維度描繪的含義。你正在一邊重構(gòu)一邊給后人挖坑。

在互聯(lián)網(wǎng)開(kāi)發(fā)“小步快跑,迭代試錯(cuò)”的大環(huán)境下,DDD 似乎是一種比較“古老而緩慢”的思想。然而,由于互聯(lián)網(wǎng)公司也逐漸深入實(shí)體經(jīng)濟(jì),業(yè)務(wù)日益復(fù)雜,我們?cè)陂_(kāi)發(fā)中也越來(lái)越多地遇到傳統(tǒng)行業(yè)軟件開(kāi)發(fā)中所面臨的問(wèn)題。

怎么解決這個(gè)問(wèn)題呢?其實(shí)法寶就是今天的主題,領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)?。∠嘈拍阕x完本文一定會(huì)有所啟發(fā)。

DDD 介紹

DDD 全程是 Domain-Driven Design,中文叫領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),是一套應(yīng)對(duì)復(fù)雜軟件系統(tǒng)分析和設(shè)計(jì)的面向?qū)ο蠼7椒ㄕ摗?/p>

以前的系統(tǒng)分析和設(shè)計(jì)是分開(kāi)的,導(dǎo)致需求和成品非常容易出現(xiàn)偏差,兩者相對(duì)獨(dú)立,還會(huì)導(dǎo)致溝通困難,DDD 則打破了這種隔閡,提出了領(lǐng)域模型概念,統(tǒng)一了分析和設(shè)計(jì)編程,使得軟件能夠更靈活快速跟隨需求變化。

( 公眾號(hào):架構(gòu)精進(jìn) )

DDD 的發(fā)展史

相信之前或多或少一定聽(tīng)說(shuō)過(guò)領(lǐng)域驅(qū)動(dòng)(DDD),繁多的概念會(huì)不會(huì)讓你眼花繚亂?抽象的邏輯是不是感覺(jué)缺少落地實(shí)踐?可能這也是 DDD 一直沒(méi)得到盛行的原因吧。

話說(shuō) 1967 年有了 OOP,1982 年有了 OOAD(面向?qū)ο蠓治龊驮O(shè)計(jì)),它是成熟版的 OOP,目標(biāo)就是解決復(fù)雜業(yè)務(wù)場(chǎng)景,這個(gè)過(guò)程中逐漸形成了一個(gè)領(lǐng)域驅(qū)動(dòng)的思潮,一轉(zhuǎn)眼到 2003 年的時(shí)候,Eric Evans 發(fā)表了一篇著作 Domain-driven Design: Tackling Complexity in the Heart of Software,正式定義了領(lǐng)域的概念,開(kāi)始了 DDD 的時(shí)代。算下來(lái)也有接近 20 年的時(shí)間了,但是,事實(shí)并不像 Eric Evans 設(shè)想的那樣容易,DDD 似乎一直不溫不火,沒(méi)有能“風(fēng)靡全球”。

2013 年,Vaughn Vernon 寫(xiě)了一本 Implementing Domain-Driven Design 進(jìn)一步定義了 DDD 的領(lǐng)域方向,并且給出了很多落地指導(dǎo),它讓人們離 DDD 又進(jìn)了一步。

同時(shí)期,隨著互聯(lián)網(wǎng)的興起,Rod Johnson 這大哥以輕量級(jí)極簡(jiǎn)風(fēng)格的 Spring Cloud 搶占了所有風(fēng)頭,雖然 Spring 推崇的失血模式并非 OOP 的皇家血統(tǒng),但是誰(shuí)用關(guān)心這些呢?畢竟簡(jiǎn)化開(kāi)發(fā)的成本才是硬道理。

就在我們用這張口閉口 Spring 的時(shí)候,我們意識(shí)到了一個(gè)嚴(yán)重的問(wèn)題,我們應(yīng)對(duì)復(fù)雜業(yè)務(wù)場(chǎng)景的時(shí)候,Spring 似乎并不能給出更合理的解決方案,于是分而治之的思想下應(yīng)生了微服務(wù),一改以往單體應(yīng)用為多個(gè)子應(yīng)用,一下子讓人眼前一亮,于是我們沒(méi)日沒(méi)夜地拆分服務(wù),加之微服務(wù)提供的注冊(cè)中心、熔斷、限流等解決方案,我們用得不亦樂(lè)乎。

人們?cè)诓冗^(guò)諸多拆分服務(wù)的坑(拆分過(guò)細(xì)導(dǎo)致服務(wù)爆炸、拆分不合理導(dǎo)致頻分重構(gòu)等)之后,開(kāi)始死鎖原因了,到底有沒(méi)有一種方法論可以指導(dǎo)人們更加合理地拆分服務(wù)呢?眾里尋他千百度,DDD 卻在燈火闌珊處,有了 DDD 的指導(dǎo),加之微服務(wù)的事件,才是完美的架構(gòu)。

DDD 與微服務(wù)的關(guān)系

背景中我們說(shuō)到,有 DDD 的指導(dǎo),加之微服務(wù)的事件,才是完美的架構(gòu),這里就詳細(xì)說(shuō)下它們的關(guān)系。

系統(tǒng)的復(fù)雜度越來(lái)越來(lái)高是必然趨勢(shì),原因可能來(lái)自自身業(yè)務(wù)的演進(jìn),也有可能是技術(shù)的創(chuàng)新,然而一個(gè)人和團(tuán)隊(duì)對(duì)復(fù)雜性的認(rèn)知是有極限的,就像一個(gè)服務(wù)器的性能極限一樣,解決的辦法只有分而治之,將大問(wèn)題拆解為小問(wèn)題,最終突破這種極限。微服務(wù)在這方面都給出來(lái)了理論指導(dǎo)和最佳實(shí)踐,諸如注冊(cè)中心、熔斷、限流等解決方案,但微服務(wù)并沒(méi)有對(duì)“應(yīng)對(duì)復(fù)雜業(yè)務(wù)場(chǎng)景”這個(gè)問(wèn)題給出合理的解決方案,這是因?yàn)槲⒎?wù)的側(cè)重點(diǎn)是治理,而不是分。

我們都知道,架構(gòu)一個(gè)系統(tǒng)的時(shí)候,應(yīng)該從以下幾方面考慮:

功能維度

質(zhì)量維度(包括性能和可用性)

工程維度

微服務(wù)在第二個(gè)做得很好,但第一個(gè)維度和第三個(gè)維度做的不夠。這就給 DDD 了一個(gè)“可乘之機(jī)”,DDD 給出了微服務(wù)在功能劃分上沒(méi)有給出的很好指導(dǎo)這個(gè)缺陷。所以說(shuō)它們?cè)诿鎸?duì)復(fù)雜問(wèn)題和構(gòu)建系統(tǒng)時(shí)是一種互補(bǔ)的關(guān)系。

從架構(gòu)角度看,微服務(wù)中的服務(wù)所關(guān)注的范圍,正是 DDD 所推崇的六邊形架構(gòu)中的領(lǐng)域?qū)樱驼麧嵓軜?gòu)中的 entity 和 use cases 層。如下圖所示:

( 公眾號(hào):( 公眾號(hào):架構(gòu)精進(jìn) ) )

DDD 與微服務(wù)如何協(xié)作

知道了 DDD 與微服務(wù)還不夠,我們還需要知道他們是怎么協(xié)作的。

一個(gè)系統(tǒng)(或者一個(gè)公司)的業(yè)務(wù)范圍和在這個(gè)范圍里進(jìn)行的活動(dòng),被稱(chēng)之為領(lǐng)域,領(lǐng)域是現(xiàn)實(shí)生活中面對(duì)的問(wèn)題域,和軟件系統(tǒng)無(wú)關(guān),領(lǐng)域可以劃分為子域,比如電商領(lǐng)域可以劃分為商品子域、訂單子域、發(fā)票子域、庫(kù)存子域 等,在不同子域里,不同概念會(huì)有不同的含義,所以我們?cè)诮5臅r(shí)候必須要有一個(gè)明確的邊界,這個(gè)邊界在 DDD 中被稱(chēng)之為限界上下文,它是系統(tǒng)架構(gòu)內(nèi)部的一個(gè)邊界,《整潔之道》這本書(shū)里提到:

系統(tǒng)架構(gòu)是由系統(tǒng)內(nèi)部的架構(gòu)邊界,以及邊界之間的依賴(lài)關(guān)系所定義的,與系統(tǒng)中組件之間的調(diào)用方式無(wú)關(guān)。

所謂的服務(wù)本身只是一種比函數(shù)調(diào)用方式成本稍高的,分割應(yīng)用程序行為的一種形式,與系統(tǒng)架構(gòu)無(wú)關(guān)。

所以復(fù)雜系統(tǒng)劃分的第一要素就是劃分系統(tǒng)內(nèi)部架構(gòu)邊界,也就是劃分上下文,以及明確之間的關(guān)系,這對(duì)應(yīng)之前說(shuō)的第一維度(功能維度),這就是 DDD 的用武之處。其次,我們才考慮基于非功能的維度如何劃分,這才是微服務(wù)發(fā)揮優(yōu)勢(shì)的地方。

假如我們把服務(wù)劃分成 ABC 三個(gè)上下文:

( 公眾號(hào):架構(gòu)精進(jìn) )

我們可以在一個(gè)進(jìn)程內(nèi)部署單體應(yīng)用,也可以通過(guò)遠(yuǎn)程調(diào)用來(lái)完成功能調(diào)用,這就是目前的微服務(wù)方式,更多的時(shí)候我們是兩種方式的混合,比如 A 和 B 在一個(gè)部署單元內(nèi),C 單獨(dú)部署,這是因?yàn)?C 非常重要,或并發(fā)量比較大,或需求變更比較頻繁,這時(shí)候 C 獨(dú)立部署有幾個(gè)好處:

C 獨(dú)立部署資源:資源更合理的傾斜,獨(dú)立擴(kuò)容縮容。

彈力服務(wù):重試、熔斷、降級(jí)等,已達(dá)到故障隔離。

技術(shù)棧獨(dú)立:C 可以使用其他語(yǔ)言編寫(xiě),更合適個(gè)性化團(tuán)隊(duì)技術(shù)棧。

團(tuán)隊(duì)獨(dú)立:可以由不同團(tuán)隊(duì)負(fù)責(zé)。

架構(gòu)是可以演進(jìn)的,所以拆分需要考慮架構(gòu)的階段,早期更注重業(yè)務(wù)邏輯邊界,后期需要考慮更多方面,比如數(shù)據(jù)量、復(fù)雜性等,但即使有這個(gè)方針,也常會(huì)見(jiàn)仁見(jiàn)智,沒(méi)有人能一下子將邊界定義正確,其實(shí)這里根本就沒(méi)有明確的對(duì)錯(cuò)。

即使邊界定義的不太合適,通過(guò)聚合根可以保障我們能夠演進(jìn)出更合適的上下文,在上下文內(nèi)部通過(guò)實(shí)體和值對(duì)象來(lái)對(duì)領(lǐng)域概念進(jìn)行建模,一組實(shí)體和值對(duì)象歸屬于一個(gè)聚合根。

按照 DDD 的約束要求:

第一,聚合根來(lái)保證內(nèi)部實(shí)體規(guī)則的正確性和數(shù)據(jù)一致性;

第二,外部對(duì)象只能通過(guò) id 來(lái)引用聚合根,不能引用聚合根內(nèi)部的實(shí)體;

第三,聚合根之間不能共享一個(gè)數(shù)據(jù)庫(kù)事務(wù),他們之間的數(shù)據(jù)一致性需要通過(guò)最終一致性來(lái)保證。

有了聚合根,再基于這些約束,未來(lái)可以根據(jù)需要,把聚合根升級(jí)為上下文,甚至拆分成微服務(wù),都是比較容易的。

DDD 的相關(guān)術(shù)語(yǔ)與基本概念

討論完宏觀概念以后,讓我們來(lái)認(rèn)識(shí)一下 DDD 的一些概念吧,每個(gè)概念我都為你找了一個(gè) Spring 模式開(kāi)發(fā)的映射概念,方便你理解,但要僅僅作為理解用,不要過(guò)于依賴(lài)。

另外,這里你可能需要結(jié)合后面的代碼反復(fù)結(jié)合理解,才能融匯貫通到實(shí)際工作中。

領(lǐng)域

映射概念:切分的服務(wù)。

領(lǐng)域就是范圍。范圍的重點(diǎn)是邊界。領(lǐng)域的核心思想是將問(wèn)題逐級(jí)細(xì)分來(lái)減低業(yè)務(wù)和系統(tǒng)的復(fù)雜度,這也是 DDD 討論的核心。

子域

映射概念:子服務(wù)。

領(lǐng)域可以進(jìn)一步劃分成子領(lǐng)域,即子域。這是處理高度復(fù)雜領(lǐng)域的設(shè)計(jì)思想,它試圖分離技術(shù)實(shí)現(xiàn)的復(fù)雜性。這個(gè)拆分的里面在很多架構(gòu)里都有,比如 C4。

核心域

映射概念:核心服務(wù)。

在領(lǐng)域劃分過(guò)程中,會(huì)不斷劃分子域,子域按重要程度會(huì)被劃分成三類(lèi):核心域、通用域、支撐域。

決定產(chǎn)品核心競(jìng)爭(zhēng)力的子域就是核心域,沒(méi)有太多個(gè)性化訴求。

桃樹(shù)的例子,有根、莖、葉、花、果、種子等六個(gè)子域,不同人理解的核心域不同,比如在果園里,核心域就是果是核心域,在公園里,核心域則是花。有時(shí)為了核心域的營(yíng)養(yǎng)供應(yīng),還會(huì)剪掉通用域和支撐域(莖、葉等)。

通用域

映射概念:中間件服務(wù)或第三方服務(wù)。

被多個(gè)子域使用的通用功能就是通用域,沒(méi)有太多企業(yè)特征,比如權(quán)限認(rèn)證。

支撐域

映射概念:企業(yè)公共服務(wù)。

對(duì)于功能來(lái)講是必須存在的,但它不對(duì)產(chǎn)品核心競(jìng)爭(zhēng)力產(chǎn)生影響,也不包含通用功能,有企業(yè)特征,不具有通用性,比如數(shù)據(jù)代碼類(lèi)的數(shù)字字典系統(tǒng)。

統(tǒng)一語(yǔ)言

映射概念:統(tǒng)一概念。

定義上下文的含義。它的價(jià)值是可以解決交流障礙,不管你是 RD、PM、QA 等什么角色,讓每個(gè)團(tuán)隊(duì)使用統(tǒng)一的語(yǔ)言(概念)來(lái)交流,甚至可讀性更好的代碼。

通用語(yǔ)言包含屬于和用例場(chǎng)景,并且能直接反應(yīng)在代碼中。

可以在事件風(fēng)暴(開(kāi)會(huì))中來(lái)統(tǒng)一語(yǔ)言,甚至是中英文的映射、業(yè)務(wù)與代碼模型的映射等??梢允褂靡粋€(gè)表格來(lái)記錄。

限界上下文

映射概念:服務(wù)職責(zé)劃分的邊界。

定義上下文的邊界。領(lǐng)域模型存在邊界之內(nèi)。對(duì)于同一個(gè)概念,不同上下文會(huì)有不同的理解,比如商品,在銷(xiāo)售階段叫商品,在運(yùn)輸階段就叫貨品。

( 公眾號(hào):架構(gòu)精進(jìn) )

理論上,限界上下文的邊界就是微服務(wù)的邊界,因此,理解限界上下文在設(shè)計(jì)中非常重要。

聚合

映射概念:包。

聚合概念類(lèi)似于你理解的包的概念,每個(gè)包里包含一類(lèi)實(shí)體或者行為,它有助于分散系統(tǒng)復(fù)雜性,也是一種高層次的抽象,可以簡(jiǎn)化對(duì)領(lǐng)域模型的理解。

拆分的實(shí)體不能都放在一個(gè)服務(wù)里,這就涉及到了拆分,那么有拆分就有聚合。聚合是為了保證領(lǐng)域內(nèi)對(duì)象之間的一致性問(wèn)題。

在定義聚合的時(shí)候,應(yīng)該遵守不變形約束法則:

聚合邊界內(nèi)必須具有哪些信息,如果沒(méi)有這些信息就不能稱(chēng)為一個(gè)有效的聚合;

聚合內(nèi)的某些對(duì)象的狀態(tài)必須滿(mǎn)足某個(gè)業(yè)務(wù)規(guī)則:

一個(gè)聚合只有一個(gè)聚合根,聚合根是可以獨(dú)立存在的,聚合中其他實(shí)體或值對(duì)象依賴(lài)與聚合根。

只有聚合根才能被外部訪問(wèn)到,聚合根維護(hù)聚合的內(nèi)部一致性。

聚合根

映射概念:包。

一個(gè)上下文內(nèi)可能包含多個(gè)聚合,每個(gè)聚合都有一個(gè)根實(shí)體,叫做聚合根,一個(gè)聚合只有一個(gè)聚合根。

實(shí)體

映射概念:Domain 或 entity。

《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)模式、原理與實(shí)踐》一書(shū)中講到,實(shí)體是具有身份和連貫性的領(lǐng)域概念,可以看出,實(shí)體其實(shí)也是一種特殊的領(lǐng)域,這里我們需要注意兩點(diǎn):唯一標(biāo)示(身份)、連續(xù)性。兩者缺一不可。

你可以想象,文章可以是實(shí)體,作者也可以是,因?yàn)樗鼈冇?id 作為唯一標(biāo)示。

值對(duì)象

映射概念:Domain 或 entity。

為了更好地展示領(lǐng)域模型之間的關(guān)系,制定的一個(gè)對(duì)象,本質(zhì)上也是一種實(shí)體,但相對(duì)實(shí)體而言,它沒(méi)有狀態(tài)和身份標(biāo)識(shí),它存在的目的就是為了表示一個(gè)值,通常使用值對(duì)象來(lái)傳達(dá)數(shù)量的形式來(lái)表示。

比如 money,讓它具有 id 顯然是不合理的,你也不可能通過(guò) id 查詢(xún)一個(gè) money。

定義值對(duì)象要依照具體場(chǎng)景的區(qū)分來(lái)看,你甚至可以把 Article 中的 Author 當(dāng)成一個(gè)值對(duì)象,但一定要清楚,Author 獨(dú)立存在的時(shí)候是實(shí)體,或者要拿 Author 做復(fù)雜的業(yè)務(wù)邏輯,那么 Author 也會(huì)升級(jí)為聚合根。

最后,給出摘自網(wǎng)絡(luò)的一張圖,比較全,索性就直接 copy 過(guò)來(lái)了,便于你宏觀回顧 DDD 的相關(guān)概念:

( 公眾號(hào):架構(gòu)精進(jìn) )

四種 Domain 模式

除了晦澀難懂的概念外,讓我們最難接受的可能就是模型的運(yùn)用了,Spring 思想中,Domain 只是數(shù)據(jù)的載體,所有行為都在 Service 中使用 Domain 封裝后流轉(zhuǎn),而 OOP 講究一對(duì)象維度來(lái)執(zhí)行業(yè)務(wù),所以,DDD 中的對(duì)象是用行為的(理解這點(diǎn)非常重要哦)。

這里我為你總結(jié)了全部的四種領(lǐng)域模式,供你區(qū)分和理解:

失血模型

貧血模型

充血模型

脹血模型

背景

先說(shuō)明一下示例背景,由于公司項(xiàng)目不能外泄的原因,我這里模擬一個(gè)文章管理系統(tǒng)(這個(gè)系統(tǒng)相對(duì)簡(jiǎn)單,理論上可以不使用 DDD,在這里僅做舉例),業(yè)務(wù)需求有:發(fā)布文章、修改文章、文章分類(lèi)搜索和展示等。

使用 Spring 開(kāi)發(fā)的話,你腦海中一定浮現(xiàn)的是如下代碼。

文章類(lèi):Article

public class Article implements Serializable {

? ? private Integer id;

? ? private String title;

? ? private Integer classId;

? ? private Integer authorId;

? ? private String authorName;

? ? private String content;

? ? private Date pubDate;

? ? //getter/setter/toString

}

DAO 類(lèi):ArticleDao/ArticleImpl

public interface ArticleDao extends BaseDao<Article>{

? ? //...

}

Repository("articleDao")

public class ArticleDaoImpl implements ArticleDao{

? ? //...

}

Service 類(lèi):ArticleService

public interface ArticleService extends BaseService<Article>{

? ? //...

}

@Service(value="articleService")

public class ArticleServiceImpl implements ArticleService {

? ? //...

}

Controller 類(lèi):略。

四種模式示例

失血模型

Domain Object 只有屬性的 getter/setter 方法的純數(shù)據(jù)類(lèi),所有的業(yè)務(wù)邏輯完全由 business object 來(lái)完成。

public class Article implements Serializable {

? ? private Integer id;

? ? private String title;

? ? private Integer classId;

? ? private Integer authorId;

? ? private String authorName;

? ? private String content;

? ? private Date pubDate;

? ? //getter/setter/toString

}

public interface ArticleDao {

? ? public Article getArticleById(Integer id);

? ? public Article findAll();

? ? public void updateArticle(Article article);

}

貧血模型

簡(jiǎn)單來(lái)說(shuō),就是 Domain Object 包含了不依賴(lài)于持久化的領(lǐng)域邏輯,而那些依賴(lài)持久化的領(lǐng)域邏輯被分離到 Service 層。

public class Article implements Serializable {

? ? private Integer id;

? ? private String title;

? ? private Integer classId;

? ? private Integer authorId;

? ? private String authorName;

? ? private String content;

? ? private Date pubDate;

? ? //getter/setter/toString

? ? //判斷是否是熱門(mén)分類(lèi)(假設(shè)等于57或102的類(lèi)別的文章就是熱門(mén)分類(lèi)的文章)

? ? public boolean isHotClass(Article article){

? ? ? ? return Stream.of(57,102)

? ? ? ? ? ? .anyMatch(classId -> classId.equals(article.getClassId()));

? ? }

? ? //更新分類(lèi),但未持久化,這里不能依賴(lài)Dao去操作實(shí)體化

? ? public Article changeClass(Article article, ArticleClass ac){

? ? ? ? return article.setClassId(ac.getId());

? ? }

}

@Repository("articleDao")

public class ArticleDaoImpl implements ArticleDao{

? ? @Resource

? ? private ArticleDao articleDao;

? ? public void changeClass(Article article, ArticleClass ac){

? ? ? ? article.changeClass(article, ac);

? ? ? ? articleDao.update(article)

? ? }

}

注意這個(gè)模式不在 Domain 層里依賴(lài) DAO。持久化的工作還需要在 DAO 或者 Service 中進(jìn)行。

這樣做的優(yōu)缺點(diǎn)

優(yōu)點(diǎn):各層單向依賴(lài),結(jié)構(gòu)清晰。

缺點(diǎn):

Domain Object 的部分比較緊密依賴(lài)的持久化 Domain Logic 被分離到 Service 層,顯得不夠 OO

Service 層過(guò)于厚重

充血模型

充血模型和第二種模型差不多,區(qū)別在于業(yè)務(wù)邏輯劃分,將絕大多數(shù)業(yè)務(wù)邏輯放到 Domain 中,Service 是很薄的一層,封裝少量業(yè)務(wù)邏輯,并且不和 DAO 打交道:

Service (事務(wù)封裝) —> Domain Object <—> DAO

public class Article implements Serializable {

? ? @Resource

? ? private static ArticleDao articleDao;

? ? private Integer id;

? ? private String title;

? ? private Integer classId;

? ? private Integer authorId;

? ? private String authorName;

? ? private String content;

? ? private Date pubDate;

? ? //getter/setter/toString

? ? //使用articleDao進(jìn)行持久化交互

? ? public List<Article> findAll(){

? ? ? ? return articleDao.findAll();

? ? }

? ? //判斷是否是熱門(mén)分類(lèi)(假設(shè)等于57或102的類(lèi)別的文章就是熱門(mén)分類(lèi)的文章)

? ? public boolean isHotClass(Article article){

? ? ? ? return Stream.of(57,102)

? ? ? ? ? ? .anyMatch(classId -> classId.equals(article.getClassId()));

? ? }

? ? //更新分類(lèi),但未持久化,這里不能依賴(lài)Dao去操作實(shí)體化

? ? public Article changeClass(Article article, ArticleClass ac){

? ? ? ? return article.setClassId(ac.getId());

? ? }

}

所有業(yè)務(wù)邏輯都在 Domain 中,事務(wù)管理也在 Item 中實(shí)現(xiàn)。這樣做的優(yōu)缺點(diǎn)如下。

優(yōu)點(diǎn):

更加符合 OO 的原則;

Service 層很薄,只充當(dāng) Facade 的角色,不和 DAO 打交道。

缺點(diǎn):

DAO 和 Domain Object 形成了雙向依賴(lài),復(fù)雜的雙向依賴(lài)會(huì)導(dǎo)致很多潛在的問(wèn)題。

如何劃分 Service 層邏輯和 Domain 層邏輯是非常含混的,在實(shí)際項(xiàng)目中,由于設(shè)計(jì)和開(kāi)發(fā)人員的水平差異,可能 導(dǎo)致整個(gè)結(jié)構(gòu)的混亂無(wú)序。

脹血模型

基于充血模型的第三個(gè)缺點(diǎn),有同學(xué)提出,干脆取消 Service 層,只剩下 Domain Object 和 DAO 兩層,在 Domain Object 的 Domain Logic 上面封裝事務(wù)。

Domain Object (事務(wù)封裝,業(yè)務(wù)邏輯) <—> DAO

似乎 Ruby on rails 就是這種模型,它甚至把 Domain Object 和 DAO 都合并了。

這樣做的優(yōu)缺點(diǎn):

簡(jiǎn)化了分層

也算符合 OO

該模型缺點(diǎn):

很多不是 Domain Logic 的 Service 邏輯也被強(qiáng)行放入 Domain Object ,引起了 Domain Object 模型的不穩(wěn)定;

Domain Object 暴露給 Web 層過(guò)多的信息,可能引起意想不到的副作用。

運(yùn)用 DDD 改造現(xiàn)有舊系統(tǒng)實(shí)踐

假如你是一個(gè)團(tuán)隊(duì) Leader 或者架構(gòu)師,當(dāng)你接手一個(gè)舊系統(tǒng)維護(hù)及重構(gòu)的任務(wù)時(shí),你該如何改造呢?是否覺(jué)得哪里都不對(duì)但由于業(yè)務(wù)認(rèn)知的不熟悉而無(wú)從下手呢?其實(shí)這里我可以教你一套方法來(lái)應(yīng)對(duì)這種窘境。

你要做的大概以下幾點(diǎn):

1. 通過(guò)公共平臺(tái)大概梳理出系統(tǒng)之間的調(diào)用關(guān)系(一般中等以上公司都具備 RPC 和 HTTP 調(diào)用關(guān)系,無(wú)腦的挨個(gè)系統(tǒng)查詢(xún)即可),畫(huà)出來(lái)的可能會(huì)很亂,也可能會(huì)比較清晰,但這就是現(xiàn)狀。

( 公眾號(hào):架構(gòu)精進(jìn) )

2. 分配組員每個(gè)人認(rèn)領(lǐng)幾個(gè)項(xiàng)目,來(lái)梳理項(xiàng)目維度關(guān)系,這些關(guān)系包括:對(duì)外接口、交互、用例、MQ 等的詳細(xì)說(shuō)明。個(gè)別核心系統(tǒng)可以畫(huà)出內(nèi)部實(shí)體或者聚合根。

3. 小組開(kāi)會(huì),挨個(gè) review 每個(gè)系統(tǒng)的業(yè)務(wù)概念,達(dá)到組內(nèi)統(tǒng)一語(yǔ)言。

( 公眾號(hào):架構(gòu)精進(jìn) )


4. 根據(jù)以上資料,即可看出哪些不合理的調(diào)用關(guān)系(比如循環(huán)調(diào)用、不規(guī)范的調(diào)用等),甚至不合理的分層。

5. 根據(jù)主線業(yè)務(wù)自頂向下細(xì)分領(lǐng)域,以及限界上下文。此過(guò)程可能會(huì)顛覆之前的系統(tǒng)劃分。

6. 根據(jù)業(yè)務(wù)復(fù)雜性,指定領(lǐng)域模型,選擇貧血或者充血模型。團(tuán)隊(duì)內(nèi)部最好實(shí)行統(tǒng)一習(xí)慣,以免出現(xiàn)交接成本過(guò)大。

7. 分工進(jìn)行開(kāi)發(fā),并設(shè)置 deadline,注意,不要單一的設(shè)置一個(gè) deadline,要設(shè)置中間 check 時(shí)間,比如 dealline 是 1 月 20 日,還要設(shè)置兩個(gè) check 時(shí)間,分別溝通代碼風(fēng)格及邊界職責(zé),以免 deadline 時(shí)延期。

DDD 與 Spring 家族的完美結(jié)合

還用前面提到的文章管理系統(tǒng),我為你說(shuō)明一下 DDD 開(kāi)發(fā)的關(guān)注點(diǎn)。

模塊(Module)

模塊(Module)是 DDD 中明確提到的一種控制限界上下文的手段,在我們的工程中,一般盡量用一個(gè)模塊來(lái)表示一個(gè)領(lǐng)域的限界上下文。

如代碼中所示,一般的工程中包的組織方式為 {com.公司名.組織架構(gòu).業(yè)務(wù).上下文.*},這樣的組織結(jié)構(gòu)能夠明確地將一個(gè)上下文限定在包的內(nèi)部。

import com.company.team.bussiness.counter.*;//計(jì)數(shù)上下文

import com.company.team.bussiness.category.*;//分類(lèi)上下文

import com.company.team.bussiness.comment.*;//評(píng)論上下文

對(duì)于模塊內(nèi)的組織結(jié)構(gòu),一般情況下我們是按照領(lǐng)域?qū)ο?、領(lǐng)域服務(wù)、領(lǐng)域資源庫(kù)、防腐層等組織方式定義的。

import com.company.team.bussiness.cms.domain.valobj.*;//領(lǐng)域?qū)ο?值對(duì)象

import com.company.team.bussiness.cms.domain.entity.*;//領(lǐng)域?qū)ο?實(shí)體

import com.company.team.bussiness.cms.domain.aggregate.*;//領(lǐng)域?qū)ο?聚合根

import com.company.team.bussiness.cms.service.*;//領(lǐng)域服務(wù)

import com.company.team.bussiness.cms.repo.*;//領(lǐng)域資源庫(kù)

import com.company.team.bussiness.cms.facade.*;//領(lǐng)域防腐層

領(lǐng)域?qū)ο?/p>

領(lǐng)域驅(qū)動(dòng)要解決的一個(gè)重要的問(wèn)題,就是解決對(duì)象的貧血問(wèn)題,而領(lǐng)域?qū)ο髣t最直接的反應(yīng)了這個(gè)能力。

我們可以定義聚合根(文章)和值對(duì)象(計(jì)數(shù)器),來(lái)舉例說(shuō)明。聚合根持有文章的 id 和文章的計(jì)數(shù)數(shù)據(jù),這里計(jì)數(shù)器之所以被列為值對(duì)象,而非實(shí)體的一個(gè)屬性,是因?yàn)橛?jì)數(shù)器是由多部分組成的,比如真實(shí)閱讀量、推廣閱讀量等。

在文章領(lǐng)域?qū)ο笾校覀冃枰x個(gè)一個(gè)方法,來(lái)獲取文章的計(jì)數(shù)量,用于頁(yè)面上顯示,這個(gè)邏輯可能會(huì)很復(fù)雜,涉及到爆文、專(zhuān)欄作者級(jí)別、發(fā)布時(shí)間等因素。

package com.company.team.bussiness.domain.aggregate;

import ...;

public class Article {

? ? @Resource

? ? private CategoryRepository categoryRepository;

? ? private int articleId; //文章id

? ? ...

? ? private ArticleCount articleCount; //文章計(jì)數(shù)器

? ? //getter & setter

? ? //查詢(xún)計(jì)數(shù)顯示數(shù)量,這里簡(jiǎn)化一些邏輯,甚至是不符合實(shí)際業(yè)務(wù)場(chǎng)景,這不重要,這里只為直觀表達(dá)意思

? ? public Integer getShowArticleCount() {

? ? ? ? ? ? if(this.articleCount == null){

? ? ? ? ? ? return 0;

? ? ? ? }

? ? ? ? return this.articleCount.realCount + categoryRepository.getCategoryWeight(this.category) + (this.articleCount.adCount * DayUtils.calDaysByNow(this.articleCount.deadDays));

? ? }

}

與以往的僅有 getter、setter 的業(yè)務(wù)對(duì)象不同,領(lǐng)域?qū)ο缶哂辛诵袨?,?duì)象更加豐滿(mǎn)。同時(shí),比起將這些邏輯寫(xiě)在服務(wù)內(nèi)(例如 Service),領(lǐng)域功能的內(nèi)聚性更強(qiáng),職責(zé)更加明確。

資源庫(kù)

領(lǐng)域?qū)ο笮枰Y源存儲(chǔ),資源庫(kù)可以理解成 DAO,但它比 DAO 更寬泛,存儲(chǔ)的手段可以是多樣化的,常見(jiàn)的無(wú)非是數(shù)據(jù)庫(kù)、分布式緩存、本地緩存等。資源庫(kù)(Repository)的作用,就是對(duì)領(lǐng)域的存儲(chǔ)和訪問(wèn)進(jìn)行統(tǒng)一管理的對(duì)象。

在系統(tǒng)中,我們是通過(guò)如下的方式組織資源庫(kù)的。

import com.company.team.bussiness.repo.dao.ArticleDao;//數(shù)據(jù)庫(kù)訪問(wèn)對(duì)象-文章

import com.company.team.bussiness.repo.dao.CommentDao;//數(shù)據(jù)庫(kù)訪問(wèn)對(duì)象-評(píng)論

import com.company.team.bussiness.repo.dao.po.ArticlePO;//數(shù)據(jù)庫(kù)持久化對(duì)象-文章

import com.company.team.bussiness.repo.dao.po.CommentPO;//數(shù)據(jù)庫(kù)持久化對(duì)象-評(píng)論

import com.company.team.bussiness.repo.cache.ArticleObj;//分布式緩存訪問(wèn)對(duì)象-文章緩存訪問(wèn)

資源庫(kù)對(duì)外的整體訪問(wèn)由 Repository 提供,它聚合了各個(gè)資源庫(kù)的數(shù)據(jù)信息,同時(shí)也承擔(dān)了資源存儲(chǔ)的邏輯(例如緩存更新機(jī)制等)。

在資源庫(kù)中,我們屏蔽了對(duì)底層獎(jiǎng)池和獎(jiǎng)品的直接訪問(wèn),而是僅對(duì)文章的聚合根進(jìn)行資源管理。代碼示例中展示了資源獲取的方法(最常見(jiàn)的 Cache Aside Pattern)。

package com.company.team.bussiness.repo;

import ...;

@Repository

public class ArticleRepository {

? ? @Autowired

? ? private ArticleDao articleDao;

? ? @AutoWired

? ? private articleDaoCacheAccessObj articleCacheAccessObj;

? ? public Article getArticleById(int articleId) {

? ? ? ? Article article = articleCacheAccessObj.get(articleId);

? ? ? ? if(article!=null){

? ? ? ? ? ? return article;

? ? ? ? }

? ? ? ? article = getArticleFromDB(articleId);

? ? ? ? articleCacheAccessObj.add(articleId, article);

? ? ? ? return article;

? ? }

? ? private Article getArticleFromDB(int articleId) {...}

}

比起以往將資源管理放在服務(wù)中的做法,由資源庫(kù)對(duì)資源進(jìn)行管理,職責(zé)更加明確,代碼的可讀性和可維護(hù)性也更強(qiáng)。

防腐層

亦稱(chēng)適配層。在一個(gè)上下文中,有時(shí)需要對(duì)外部上下文進(jìn)行訪問(wèn),通常會(huì)引入防腐層的概念來(lái)對(duì)外部上下文的訪問(wèn)進(jìn)行一次轉(zhuǎn)義。

有以下幾種情況會(huì)考慮引入防腐層:

需要將外部上下文中的模型翻譯成本上下文理解的模型。

不同上下文之間的團(tuán)隊(duì)協(xié)作關(guān)系,如果是供奉者關(guān)系,建議引入防腐層,避免外部上下文變化對(duì)本上下文的侵蝕。

該訪問(wèn)本上下文使用廣泛,為了避免改動(dòng)影響范圍過(guò)大。

package com.company.team.bussiness.facade;

import ...;

@Component

public class ArticleFacade {

? ? @Resource

? ? private ArticleService articleService;

? ? public Article getArticle(ArticleContext context) {

? ? ? ? ArticleResponse resp = articleService.getArticle(context.getArticleId());

? ? ? ? return buildArticle(resp);

? ? }

? ? private Article buildArticle(ArticleResponse resp) {...}

}

如果內(nèi)部多個(gè)上下文對(duì)外部上下文需要訪問(wèn),那么可以考慮將其放到通用上下文中。

領(lǐng)域服務(wù)

上文中,我們將領(lǐng)域行為封裝到領(lǐng)域?qū)ο笾校瑢①Y源管理行為封裝到資源庫(kù)中,將外部上下文的交互行為封裝到防腐層中。此時(shí),我們?cè)倩剡^(guò)頭來(lái)看領(lǐng)域服務(wù)時(shí),能夠發(fā)現(xiàn)領(lǐng)域服務(wù)本身所承載的職責(zé)也就更加清晰了,即就是通過(guò)串聯(lián)領(lǐng)域?qū)ο?、資源庫(kù)和防腐層等一系列領(lǐng)域內(nèi)的對(duì)象的行為,對(duì)其他上下文提供交互的接口。

package com.company.team.bussiness.service.impl

import ...;

@Service

public class CommentServiceImpl implements CommentService {

? ? ? @Resource

? ? private CommentFacade commentFacade;

? ? ? @Resource

? ? private ArticleRepository articleRepo;

? ? @Resource

? ? private ArticleService articleService;

? ? @Override

? ? public CommentResponse commentArticle(CommentContext commentContext) {

? ? ? ? Article article = articleRepo.getArticleById(commentContext.getArticleId());//獲取文章聚合根

? ? ? ? commentFacade.doComment(commentContext);//增加計(jì)數(shù)信息

? ? ? ? return buildCommentResponse(commentContext,article);//組裝評(píng)論后的文章信息

? ? }

? ? private CommentResponse buildCommentResponse(CommentContext commentContext, Article article) {...}

}

可以看到在省略了一些防御性邏輯(異常處理、空值判斷等)后,領(lǐng)域服務(wù)的邏輯已經(jīng)足夠清晰明了。

示范包結(jié)構(gòu)

( 公眾號(hào):架構(gòu)精進(jìn) )

反思思考

DDD 將領(lǐng)域?qū)舆M(jìn)行了細(xì)分,是 DDD 比較 MVC 框架的最大亮點(diǎn)。

DDD 能做到這一點(diǎn),主要是因?yàn)?DDD 將領(lǐng)域?qū)舆M(jìn)行了細(xì)分,比如說(shuō)領(lǐng)域?qū)ο笥袑?shí)體、聚合,動(dòng)作和操作叫做領(lǐng)域服務(wù),能力叫做領(lǐng)域能力等,而 MVC 架構(gòu)并沒(méi)有對(duì)業(yè)務(wù)元素進(jìn)行細(xì)分,所有的業(yè)務(wù)都是 Service,從而導(dǎo)致 Controller 層和 Service 層很難定義出技術(shù)約束,因?yàn)槎际?Service,你不會(huì)知道這個(gè) Service 是用來(lái)描述對(duì)象的還是來(lái)描述一個(gè)業(yè)務(wù)操作的。

針對(duì)未來(lái)業(yè)務(wù)擴(kuò)展方面,聚合根升級(jí)為上下文,甚至拆分成微服務(wù),也是應(yīng)對(duì)復(fù)雜問(wèn)題的重要手段。

實(shí)體和值對(duì)象是對(duì)現(xiàn)有編程習(xí)慣最大的變化,但不要過(guò)度關(guān)注而忽略了領(lǐng)域?qū)ο笾g的關(guān)系。

DDD 本身是方法論,是提供理論指導(dǎo)的,所以不要奢求像 Spring 那樣給你一個(gè) Demo 照著寫(xiě),希望讀者看完后多多反思。

( 公眾號(hào):架構(gòu)精進(jìn) )

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • DDD已經(jīng)火了很久,目前在很多項(xiàng)目上都有所應(yīng)用,而這次是我第一次參加DDD相關(guān)的培訓(xùn),對(duì)我來(lái)說(shuō)神秘的DDD一層一層...
    前端進(jìn)城打工仔閱讀 2,718評(píng)論 2 11
  • DDD,中文名為領(lǐng)域驅(qū)動(dòng)設(shè)計(jì),為業(yè)務(wù)開(kāi)發(fā)中必不可少的指導(dǎo)方法論,本文以業(yè)務(wù)開(kāi)發(fā)中戰(zhàn)略設(shè)計(jì)和戰(zhàn)術(shù)設(shè)計(jì)為例,將普通開(kāi)發(fā)...
    RobynnD閱讀 1,137評(píng)論 0 2
  • 窗外車(chē)來(lái)車(chē)往的喧囂, 淹蓋不了我內(nèi)心想你的吶喊。 夜色燈紅酒綠的光照, 遮蓋不了我內(nèi)心想你的絢麗。 即使是寧?kù)o的一...
    贖罪的愛(ài)閱讀 263評(píng)論 0 12
  • 這部電影,在網(wǎng)上的評(píng)論是,中國(guó)終于有像樣的類(lèi)型片了,或者有,《追兇者也》與《七月與安生》預(yù)示著中國(guó)類(lèi)型片電影的崛起...
    裊裊東風(fēng)閱讀 343評(píng)論 0 0
  • 你總是說(shuō)我不夠溫和,態(tài)度防御太重,今后如果只剩下我一個(gè)可怎么辦?我知道你擔(dān)心,除了你再也不會(huì)有人能如你一般愛(ài)我,那...
    素午閱讀 257評(píng)論 0 0

友情鏈接更多精彩內(nèi)容