DDD 模式從天書到實踐

背景

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

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

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

怎么解決這個問題呢?其實法寶就是今天的主題事镣,領(lǐng)域驅(qū)動設(shè)計2郊!相信你讀完本文一定會有所啟發(fā)璃哟。

DDD 介紹

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

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

( 公眾號:架構(gòu)精進 )

DDD 的發(fā)展史

相信之前或多或少一定聽說過領(lǐng)域驅(qū)動(DDD)泽疆,繁多的概念會不會讓你眼花繚亂?抽象的邏輯是不是感覺缺少落地實踐玲献?可能這也是 DDD 一直沒得到盛行的原因吧殉疼。

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

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

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

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

人們在踩過諸多拆分服務(wù)的坑(拆分過細導(dǎo)致服務(wù)爆炸菱魔、拆分不合理導(dǎo)致頻分重構(gòu)等)之后,開始死鎖原因了吟孙,到底有沒有一種方法論可以指導(dǎo)人們更加合理地拆分服務(wù)呢澜倦?眾里尋他千百度,DDD 卻在燈火闌珊處杰妓,有了 DDD 的指導(dǎo)藻治,加之微服務(wù)的事件,才是完美的架構(gòu)巷挥。

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

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

系統(tǒng)的復(fù)雜度越來越來高是必然趨勢高职,原因可能來自自身業(yè)務(wù)的演進钩乍,也有可能是技術(shù)的創(chuàng)新,然而一個人和團隊對復(fù)雜性的認知是有極限的怔锌,就像一個服務(wù)器的性能極限一樣寥粹,解決的辦法只有分而治之,將大問題拆解為小問題埃元,最終突破這種極限涝涤。微服務(wù)在這方面都給出來了理論指導(dǎo)和最佳實踐,諸如注冊中心岛杀、熔斷阔拳、限流等解決方案,但微服務(wù)并沒有對“應(yīng)對復(fù)雜業(yè)務(wù)場景”這個問題給出合理的解決方案类嗤,這是因為微服務(wù)的側(cè)重點是治理衫生,而不是分。

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

功能維度

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

工程維度

微服務(wù)在第二個做得很好彭羹,但第一個維度和第三個維度做的不夠黄伊。這就給 DDD 了一個“可乘之機”,DDD 給出了微服務(wù)在功能劃分上沒有給出的很好指導(dǎo)這個缺陷派殷。所以說它們在面對復(fù)雜問題和構(gòu)建系統(tǒng)時是一種互補的關(guān)系还最。

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

( 公眾號:( 公眾號:架構(gòu)精進 ) )

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

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

一個系統(tǒng)(或者一個公司)的業(yè)務(wù)范圍和在這個范圍里進行的活動勿锅,被稱之為領(lǐng)域,領(lǐng)域是現(xiàn)實生活中面對的問題域枣氧,和軟件系統(tǒng)無關(guān)溢十,領(lǐng)域可以劃分為子域,比如電商領(lǐng)域可以劃分為商品子域达吞、訂單子域张弛、發(fā)票子域、庫存子域 等酪劫,在不同子域里吞鸭,不同概念會有不同的含義,所以我們在建模的時候必須要有一個明確的邊界覆糟,這個邊界在 DDD 中被稱之為限界上下文刻剥,它是系統(tǒng)架構(gòu)內(nèi)部的一個邊界,《整潔之道》這本書里提到:

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

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

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

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

( 公眾號:架構(gòu)精進 )

我們可以在一個進程內(nèi)部署單體應(yīng)用,也可以通過遠程調(diào)用來完成功能調(diào)用埃篓,這就是目前的微服務(wù)方式处坪,更多的時候我們是兩種方式的混合,比如 A 和 B 在一個部署單元內(nèi),C 單獨部署同窘,這是因為 C 非常重要玄帕,或并發(fā)量比較大,或需求變更比較頻繁想邦,這時候 C 獨立部署有幾個好處:

C 獨立部署資源:資源更合理的傾斜裤纹,獨立擴容縮容。

彈力服務(wù):重試丧没、熔斷鹰椒、降級等,已達到故障隔離骂铁。

技術(shù)棧獨立:C 可以使用其他語言編寫吹零,更合適個性化團隊技術(shù)棧。

團隊獨立:可以由不同團隊負責(zé)拉庵。

架構(gòu)是可以演進的灿椅,所以拆分需要考慮架構(gòu)的階段,早期更注重業(yè)務(wù)邏輯邊界钞支,后期需要考慮更多方面茫蛹,比如數(shù)據(jù)量、復(fù)雜性等烁挟,但即使有這個方針婴洼,也常會見仁見智,沒有人能一下子將邊界定義正確撼嗓,其實這里根本就沒有明確的對錯柬采。

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

按照 DDD 的約束要求:

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

第二,外部對象只能通過 id 來引用聚合根杏头,不能引用聚合根內(nèi)部的實體盈包;

第三,聚合根之間不能共享一個數(shù)據(jù)庫事務(wù)醇王,他們之間的數(shù)據(jù)一致性需要通過最終一致性來保證呢燥。

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

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

討論完宏觀概念以后,讓我們來認識一下 DDD 的一些概念吧排嫌,每個概念我都為你找了一個 Spring 模式開發(fā)的映射概念畸裳,方便你理解,但要僅僅作為理解用淳地,不要過于依賴怖糊。

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

領(lǐng)域

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

領(lǐng)域就是范圍遣钳。范圍的重點是邊界扰魂。領(lǐng)域的核心思想是將問題逐級細分來減低業(yè)務(wù)和系統(tǒng)的復(fù)雜度,這也是 DDD 討論的核心蕴茴。

子域

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

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

核心域

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

在領(lǐng)域劃分過程中愿棋,會不斷劃分子域科展,子域按重要程度會被劃分成三類:核心域、通用域初斑、支撐域辛润。

決定產(chǎn)品核心競爭力的子域就是核心域,沒有太多個性化訴求见秤。

桃樹的例子砂竖,有根、莖鹃答、葉乎澄、花、果测摔、種子等六個子域置济,不同人理解的核心域不同解恰,比如在果園里,核心域就是果是核心域浙于,在公園里护盈,核心域則是花。有時為了核心域的營養(yǎng)供應(yīng)羞酗,還會剪掉通用域和支撐域(莖腐宋、葉等)。

通用域

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

被多個子域使用的通用功能就是通用域胸竞,沒有太多企業(yè)特征,比如權(quán)限認證参萄。

支撐域

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

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

統(tǒng)一語言

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

定義上下文的含義铡羡。它的價值是可以解決交流障礙积蔚,不管你是 RD、PM烦周、QA 等什么角色尽爆,讓每個團隊使用統(tǒng)一的語言(概念)來交流,甚至可讀性更好的代碼读慎。

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

可以在事件風(fēng)暴(開會)中來統(tǒng)一語言夭委,甚至是中英文的映射幅狮、業(yè)務(wù)與代碼模型的映射等≈昃模可以使用一個表格來記錄崇摄。

限界上下文

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

定義上下文的邊界慌烧。領(lǐng)域模型存在邊界之內(nèi)逐抑。對于同一個概念,不同上下文會有不同的理解屹蚊,比如商品厕氨,在銷售階段叫商品进每,在運輸階段就叫貨品。

( 公眾號:架構(gòu)精進 )

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

聚合

映射概念:包肉瓦。

聚合概念類似于你理解的包的概念,每個包里包含一類實體或者行為胃惜,它有助于分散系統(tǒng)復(fù)雜性,也是一種高層次的抽象哪雕,可以簡化對領(lǐng)域模型的理解船殉。

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

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

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

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

一個聚合只有一個聚合根钉疫,聚合根是可以獨立存在的硼讽,聚合中其他實體或值對象依賴與聚合根。

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

聚合根

映射概念:包。

一個上下文內(nèi)可能包含多個聚合城菊,每個聚合都有一個根實體备燃,叫做聚合根,一個聚合只有一個聚合根凌唬。

實體

映射概念:Domain 或 entity并齐。

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

你可以想象赐纱,文章可以是實體脊奋,作者也可以是,因為它們有 id 作為唯一標(biāo)示疙描。

值對象

映射概念:Domain 或 entity诚隙。

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

比如 money畏妖,讓它具有 id 顯然是不合理的脉执,你也不可能通過 id 查詢一個 money。

定義值對象要依照具體場景的區(qū)分來看戒劫,你甚至可以把 Article 中的 Author 當(dāng)成一個值對象半夷,但一定要清楚,Author 獨立存在的時候是實體迅细,或者要拿 Author 做復(fù)雜的業(yè)務(wù)邏輯巫橄,那么 Author 也會升級為聚合根。

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

( 公眾號:架構(gòu)精進 )

四種 Domain 模式

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

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

失血模型

貧血模型

充血模型

脹血模型

背景

先說明一下示例背景呼巷,由于公司項目不能外泄的原因,我這里模擬一個文章管理系統(tǒng)(這個系統(tǒng)相對簡單赎瑰,理論上可以不使用 DDD王悍,在這里僅做舉例),業(yè)務(wù)需求有:發(fā)布文章餐曼、修改文章压储、文章分類搜索和展示等鲜漩。

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

文章類: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 類:ArticleDao/ArticleImpl

public interface ArticleDao extends BaseDao<Article>{

? ? //...

}

Repository("articleDao")

public class ArticleDaoImpl implements ArticleDao{

? ? //...

}

Service 類:ArticleService

public interface ArticleService extends BaseService<Article>{

? ? //...

}

@Service(value="articleService")

public class ArticleServiceImpl implements ArticleService {

? ? //...

}

Controller 類:略孕似。

四種模式示例

失血模型

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

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);

}

貧血模型

簡單來說喉祭,就是 Domain Object 包含了不依賴于持久化的領(lǐng)域邏輯,而那些依賴持久化的領(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

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

? ? public boolean isHotClass(Article article){

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

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

? ? }

? ? //更新分類泛烙,但未持久化,這里不能依賴Dao去操作實體化

? ? 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)

? ? }

}

注意這個模式不在 Domain 層里依賴 DAO翘紊。持久化的工作還需要在 DAO 或者 Service 中進行胶惰。

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

優(yōu)點:各層單向依賴,結(jié)構(gòu)清晰霞溪。

缺點:

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

Service 層過于厚重

充血模型

充血模型和第二種模型差不多中捆,區(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進行持久化交互

? ? public List<Article> findAll(){

? ? ? ? return articleDao.findAll();

? ? }

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

? ? public boolean isHotClass(Article article){

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

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

? ? }

? ? //更新分類,但未持久化蟋滴,這里不能依賴Dao去操作實體化

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

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

? ? }

}

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

優(yōu)點:

更加符合 OO 的原則肖粮;

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

缺點:

DAO 和 Domain Object 形成了雙向依賴,復(fù)雜的雙向依賴會導(dǎo)致很多潛在的問題允坚。

如何劃分 Service 層邏輯和 Domain 層邏輯是非常含混的魂那,在實際項目中,由于設(shè)計和開發(fā)人員的水平差異稠项,可能 導(dǎo)致整個結(jié)構(gòu)的混亂無序涯雅。

脹血模型

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

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

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

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

簡化了分層

也算符合 OO

該模型缺點:

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

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

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

假如你是一個團隊 Leader 或者架構(gòu)師乱顾,當(dāng)你接手一個舊系統(tǒng)維護及重構(gòu)的任務(wù)時乍钻,你該如何改造呢?是否覺得哪里都不對但由于業(yè)務(wù)認知的不熟悉而無從下手呢庆寺?其實這里我可以教你一套方法來應(yīng)對這種窘境蚊夫。

你要做的大概以下幾點:

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

( 公眾號:架構(gòu)精進 )

2. 分配組員每個人認領(lǐng)幾個項目琅轧,來梳理項目維度關(guān)系,這些關(guān)系包括:對外接口踊挠、交互乍桂、用例、MQ 等的詳細說明效床。個別核心系統(tǒng)可以畫出內(nèi)部實體或者聚合根睹酌。

3. 小組開會,挨個 review 每個系統(tǒng)的業(yè)務(wù)概念剩檀,達到組內(nèi)統(tǒng)一語言憋沿。

( 公眾號:架構(gòu)精進 )


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

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

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

7. 分工進行開發(fā)便贵,并設(shè)置 deadline,注意冗荸,不要單一的設(shè)置一個 deadline承璃,要設(shè)置中間 check 時間,比如 dealline 是 1 月 20 日蚌本,還要設(shè)置兩個 check 時間盔粹,分別溝通代碼風(fēng)格及邊界職責(zé),以免 deadline 時延期程癌。

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

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

模塊(Module)

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

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

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

import com.company.team.bussiness.category.*;//分類上下文

import com.company.team.bussiness.comment.*;//評論上下文

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

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

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

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)域資源庫

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

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

領(lǐng)域驅(qū)動要解決的一個重要的問題曙旭,就是解決對象的貧血問題盗舰,而領(lǐng)域?qū)ο髣t最直接的反應(yīng)了這個能力。

我們可以定義聚合根(文章)和值對象(計數(shù)器)桂躏,來舉例說明钻趋。聚合根持有文章的 id 和文章的計數(shù)數(shù)據(jù),這里計數(shù)器之所以被列為值對象剂习,而非實體的一個屬性蛮位,是因為計數(shù)器是由多部分組成的,比如真實閱讀量鳞绕、推廣閱讀量等失仁。

在文章領(lǐng)域?qū)ο笾校覀冃枰x個一個方法们何,來獲取文章的計數(shù)量萄焦,用于頁面上顯示,這個邏輯可能會很復(fù)雜,涉及到爆文拂封、專欄作者級別茬射、發(fā)布時間等因素。

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

import ...;

public class Article {

? ? @Resource

? ? private CategoryRepository categoryRepository;

? ? private int articleId; //文章id

? ? ...

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

? ? //getter & setter

? ? //查詢計數(shù)顯示數(shù)量冒签,這里簡化一些邏輯在抛,甚至是不符合實際業(yè)務(wù)場景,這不重要萧恕,這里只為直觀表達意思

? ? 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ù)對象不同,領(lǐng)域?qū)ο缶哂辛诵袨槔扰福瑢ο蟾迂S滿望浩。同時,比起將這些邏輯寫在服務(wù)內(nèi)(例如 Service)惰说,領(lǐng)域功能的內(nèi)聚性更強磨德,職責(zé)更加明確。

資源庫

領(lǐng)域?qū)ο笮枰Y源存儲吆视,資源庫可以理解成 DAO典挑,但它比 DAO 更寬泛,存儲的手段可以是多樣化的啦吧,常見的無非是數(shù)據(jù)庫您觉、分布式緩存、本地緩存等授滓。資源庫(Repository)的作用琳水,就是對領(lǐng)域的存儲和訪問進行統(tǒng)一管理的對象。

在系統(tǒng)中般堆,我們是通過如下的方式組織資源庫的在孝。

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

import com.company.team.bussiness.repo.dao.CommentDao;//數(shù)據(jù)庫訪問對象-評論

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

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

import com.company.team.bussiness.repo.cache.ArticleObj;//分布式緩存訪問對象-文章緩存訪問

資源庫對外的整體訪問由 Repository 提供,它聚合了各個資源庫的數(shù)據(jù)信息淮摔,同時也承擔(dā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ù)中的做法魔招,由資源庫對資源進行管理晰搀,職責(zé)更加明確,代碼的可讀性和可維護性也更強办斑。

防腐層

亦稱適配層厕隧。在一個上下文中,有時需要對外部上下文進行訪問,通常會引入防腐層的概念來對外部上下文的訪問進行一次轉(zhuǎn)義吁讨。

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

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

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

該訪問本上下文使用廣泛翎朱,為了避免改動影響范圍過大橄维。

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)部多個上下文對外部上下文需要訪問,那么可以考慮將其放到通用上下文中拴曲。

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

上文中争舞,我們將領(lǐng)域行為封裝到領(lǐng)域?qū)ο笾校瑢①Y源管理行為封裝到資源庫中澈灼,將外部上下文的交互行為封裝到防腐層中竞川。此時,我們再回過頭來看領(lǐng)域服務(wù)時叁熔,能夠發(fā)現(xiàn)領(lǐng)域服務(wù)本身所承載的職責(zé)也就更加清晰了委乌,即就是通過串聯(lián)領(lǐng)域?qū)ο蟆①Y源庫和防腐層等一系列領(lǐng)域內(nèi)的對象的行為荣回,對其他上下文提供交互的接口遭贸。

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);//增加計數(shù)信息

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

? ? }

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

}

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

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

( 公眾號:架構(gòu)精進 )

反思思考

DDD 將領(lǐng)域?qū)舆M行了細分,是 DDD 比較 MVC 框架的最大亮點删铃。

DDD 能做到這一點耳贬,主要是因為 DDD 將領(lǐng)域?qū)舆M行了細分,比如說領(lǐng)域?qū)ο笥袑嶓w泳姐、聚合,動作和操作叫做領(lǐng)域服務(wù)暂吉,能力叫做領(lǐng)域能力等胖秒,而 MVC 架構(gòu)并沒有對業(yè)務(wù)元素進行細分,所有的業(yè)務(wù)都是 Service慕的,從而導(dǎo)致 Controller 層和 Service 層很難定義出技術(shù)約束阎肝,因為都是 Service,你不會知道這個 Service 是用來描述對象的還是來描述一個業(yè)務(wù)操作的肮街。

針對未來業(yè)務(wù)擴展方面风题,聚合根升級為上下文,甚至拆分成微服務(wù),也是應(yīng)對復(fù)雜問題的重要手段沛硅。

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

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

( 公眾號:架構(gòu)精進 )

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昵骤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肯适,更是在濱河造成了極大的恐慌变秦,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件框舔,死亡現(xiàn)場離奇詭異蹦玫,居然都是意外死亡,警方通過查閱死者的電腦和手機雨饺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門钳垮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人额港,你說我怎么就攤上這事饺窿。” “怎么了移斩?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵肚医,是天一觀的道長。 經(jīng)常有香客問我向瓷,道長肠套,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任猖任,我火速辦了婚禮你稚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朱躺。我一直安慰自己刁赖,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布长搀。 她就那樣靜靜地躺著宇弛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪源请。 梳的紋絲不亂的頭發(fā)上枪芒,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天彻况,我揣著相機與錄音,去河邊找鬼舅踪。 笑死纽甘,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的硫朦。 我是一名探鬼主播贷腕,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼咬展!你這毒婦竟也來了泽裳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤破婆,失蹤者是張志新(化名)和其女友劉穎涮总,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祷舀,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡瀑梗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了裳扯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抛丽。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖饰豺,靈堂內(nèi)的尸體忽然破棺而出亿鲜,到底是詐尸還是另有隱情,我是刑警寧澤冤吨,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布蒿柳,位于F島的核電站,受9級特大地震影響漩蟆,放射性物質(zhì)發(fā)生泄漏垒探。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一怠李、第九天 我趴在偏房一處隱蔽的房頂上張望圾叼。 院中可真熱鬧,春花似錦捺癞、人聲如沸夷蚊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撬码。三九已至儿倒,卻和暖如春版保,著一層夾襖步出監(jiān)牢的瞬間呜笑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工彻犁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叫胁,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓汞幢,卻偏偏與公主長得像驼鹅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子森篷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容

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