一篇帶給你DDD深入淺出
為什么我們要了解ddd?
作為一個開發(fā)者,我們肯定接手過其他的人的項目。我想你一定有個這樣的經(jīng)歷:
面對冗雜的系統(tǒng),模塊彼此關(guān)聯(lián),沒有人能描述清楚每個細(xì)節(jié),沒有文檔,即使有文檔也和系統(tǒng)對不上。當(dāng)新需求需要修改一個功能時,往往光回顧該功能涉及的流程就需要很長時間,更別提修改帶來的不可預(yù)知的影響面。于是 RD 就加開關(guān),小心翼翼地切流量上線,一有問題趕緊關(guān)閉開關(guān)。
面對此般場景,你要么跑路,要么重構(gòu)。重構(gòu)是克服演進(jìn)式設(shè)計中大雜燴問題的主力,通過在單獨的類及方法級別上做一系列小步重構(gòu)來完成,我們可以很容易重構(gòu)出一個獨立的類來放某些通用的邏輯,但是,你會發(fā)現(xiàn)你很難給它一個業(yè)務(wù)上的含義,只能給予一個技術(shù)維度描繪的含義。你正在一邊重構(gòu)一邊給后人挖坑。
作為一個架構(gòu)師,在軟件開發(fā)中如何降低系統(tǒng)復(fù)雜度是一個永恒的挑戰(zhàn),雖然通過一系列的設(shè)計模式或范例來降低一些常見的復(fù)雜度。但是問題在于,這些理念是通過技術(shù)手段解決技術(shù)問題,但并沒有從根本上解決業(yè)務(wù)的問題。
如果你也有這方面的苦惱,那么ddd 的思想也許能為你帶來啟發(fā)。
DDD 和傳統(tǒng)數(shù)據(jù)驅(qū)動的區(qū)別
DDD 全程是 Domain-Driven Design,中文叫領(lǐng)域驅(qū)動設(shè)計,是一套應(yīng)對復(fù)雜軟件系統(tǒng)分析和設(shè)計的面向?qū)ο蠼7椒ㄕ摗?/p>
什么是數(shù)據(jù)驅(qū)動
傳統(tǒng)的數(shù)據(jù)驅(qū)動開發(fā)模式,View、Service、dao這種三層分層模式,開發(fā)者會很自然的寫出過程式代碼,這種開發(fā)方式中的對象只是數(shù)據(jù)載體,而沒有行為,是一種貧血對象模型。以數(shù)據(jù)為中心,以數(shù)據(jù)庫ER圖為設(shè)計驅(qū)動,分層架構(gòu)在這種開發(fā)模式下可以認(rèn)為是數(shù)據(jù)處理和實現(xiàn)的過程。
什么是領(lǐng)域驅(qū)動
以前的系統(tǒng)分析和設(shè)計是分開的,導(dǎo)致需求和成品非常容易出現(xiàn)偏差,兩者相對獨立,還會導(dǎo)致溝通困難,DDD 則打破了這種隔閡,提出了領(lǐng)域模型概念,統(tǒng)一了分析和設(shè)計編程,使得軟件能夠更靈活快速跟隨需求變化。
DDD 的宏觀理念其實并不難懂,但是如同 REST 一樣,DDD 也只是一個設(shè)計思想,缺少一套完整的規(guī)范,導(dǎo)致DDD新手落地困難。
由于 DDD 不是一套框架,而是一種架構(gòu)思想,所以在代碼層面缺乏了足夠的約束,導(dǎo)致 DDD 在實際應(yīng)用中上手門檻很高,甚至可以說絕大部分人都對 DDD 的理解有所偏差。舉個例子(貧血域模型)在實際應(yīng)用當(dāng)中層出不窮,而一些仍然火熱的 ORM 工具比如 Hibernate,Entity Framework 實際上助長了貧血模型的擴(kuò)散。同樣的,傳統(tǒng)的基于數(shù)據(jù)庫技術(shù)以及 MVC 的四層應(yīng)用架構(gòu)(UI、Business、Data Access、Database),在一定程度上和 DDD 的一些概念混淆,導(dǎo)致絕大部分人在實際應(yīng)用當(dāng)中僅僅用到了 DDD 的建模的思想,而其對于整個架構(gòu)體系的思想無法落地。
從單機(jī)的時代,服務(wù)化的架構(gòu)還局限于單機(jī) +LB 用 MVC 提供 Rest 接口供外部調(diào)用,到今天,在一個所有的東西都能被稱之為“服務(wù)”的時代(XAAS),人們在踩過諸多拆分服務(wù)的坑(拆分過細(xì)導(dǎo)致服務(wù)爆炸、拆分不合理導(dǎo)致頻分重構(gòu)等)之后,開始死鎖原因了。DDD 的思想讓我們能冷靜下來,去思考到底哪些東西可以被服務(wù)化拆分,哪些邏輯需要聚合,才能帶來最小的維護(hù)成本,而不是簡單的去追求開發(fā)效率。
有了 DDD 的指導(dǎo),加之微服務(wù)的事件,才是完美的架構(gòu)。
DDD 與微服務(wù)的關(guān)系
系統(tǒng)的復(fù)雜度越來越來高是必然趨勢,原因可能來自自身業(yè)務(wù)的演進(jìn),也有可能是技術(shù)的創(chuàng)新,然而一個人和團(tuán)隊對復(fù)雜性的認(rèn)知是有極限的,就像一個服務(wù)器的性能極限一樣,解決的辦法只有分而治之,將大問題拆解為小問題,最終突破這種極限。微服務(wù)在這方面都給出來了理論指導(dǎo)和最佳實踐,諸如注冊中心、熔斷、限流等解決方案,但微服務(wù)并沒有對“應(yīng)對復(fù)雜業(yè)務(wù)場景”這個問題給出合理的解決方案,這是因為微服務(wù)的側(cè)重點是治理,而不是分。
我們都知道,架構(gòu)一個系統(tǒng)的時候,應(yīng)該從以下幾方面考慮:
- 功能維度
- 質(zhì)量維度(包括性能和可用性)
- 工程維度
微服務(wù)在第二個做得很好,但第一個維度和第三個維度做的不夠。這就給 DDD 了一個“可乘之機(jī)”,DDD 給出了微服務(wù)在功能劃分上沒有給出的很好指導(dǎo)這個缺陷。所以說它們在面對復(fù)雜問題和構(gòu)建系統(tǒng)時是一種互補(bǔ)的關(guān)系。
DDD 與微服務(wù)如何協(xié)作
知道了 DDD 與微服務(wù)還不夠,我們還需要知道他們是怎么協(xié)作的。
一個系統(tǒng)(或者一個公司)的業(yè)務(wù)范圍和在這個范圍里進(jìn)行的活動,被稱之為領(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 三個上下文:
我們可以在一個進(jìn)程內(nèi)部署單體應(yīng)用,也可以通過遠(yuǎn)程調(diào)用來完成功能調(diào)用,這就是目前的微服務(wù)方式,更多的時候我們是兩種方式的混合,比如 A 和 B 在一個部署單元內(nèi),C 單獨部署,這是因為 C 非常重要,或并發(fā)量比較大,或需求變更比較頻繁,這時候 C 獨立部署有幾個好處:
- C 獨立部署資源:資源更合理的傾斜,獨立擴(kuò)容縮容。
- 彈力服務(wù):重試、熔斷、降級等,已達(dá)到故障隔離。
- 技術(shù)棧獨立:C 可以使用其他語言編寫,更合適個性化團(tuán)隊技術(shù)棧。
- 團(tuán)隊獨立:可以由不同團(tuán)隊負(fù)責(zé)。
架構(gòu)是可以演進(jìn)的,所以拆分需要考慮架構(gòu)的階段,早期更注重業(yè)務(wù)邏輯邊界,后期需要考慮更多方面,比如數(shù)據(jù)量、復(fù)雜性等,但即使有這個方針,也常會見仁見智,沒有人能一下子將邊界定義正確,其實這里根本就沒有明確的對錯。
即使邊界定義的不太合適,通過聚合根可以保障我們能夠演進(jìn)出更合適的上下文,在上下文內(nèi)部通過實體和值對象來對領(lǐng)域概念進(jìn)行建模,一組實體和值對象歸屬于一個聚合根。
按照 DDD 的約束要求:
- 第一,聚合根來保證內(nèi)部實體規(guī)則的正確性和數(shù)據(jù)一致性;
- 第二,外部對象只能通過 id 來引用聚合根,不能引用聚合根內(nèi)部的實體;
- 第三,聚合根之間不能共享一個數(shù)據(jù)庫事務(wù),他們之間的數(shù)據(jù)一致性需要通過最終一致性來保證。
有了聚合根,再基于這些約束,未來可以根據(jù)需要,把聚合根升級為上下文,甚至拆分成微服務(wù),都是比較容易的。
其實DDD的核心訴求就是將業(yè)務(wù)架構(gòu)映射到系統(tǒng)架構(gòu)上,在響應(yīng)業(yè)務(wù)變化調(diào)整業(yè)務(wù)架構(gòu)時,也隨之變化系統(tǒng)架構(gòu)。而微服務(wù)追求業(yè)務(wù)層面的復(fù)用,設(shè)計出來的系統(tǒng)架構(gòu)和業(yè)務(wù)一致;在技術(shù)架構(gòu)上則系統(tǒng)模塊之間充分解耦,可以自由地選擇合適的技術(shù)架構(gòu),去中心化地治理技術(shù)和數(shù)據(jù)。
可以參見下圖來更好地理解雙方之間的協(xié)作關(guān)系:
DDD 的相關(guān)術(shù)語與基本概念
我們來認(rèn)識一下 DDD 的一些概念吧,每個概念找了一個 Spring 模式開發(fā)的映射概念,方便理解,但要僅僅作為理解用,不要過于依賴。另外,這里可能需要結(jié)合后面的代碼反復(fù)結(jié)合理解,才能融匯貫通到實際工作中。
領(lǐng)域
映射概念:切分的服務(wù)。
領(lǐng)域就是范圍。范圍的重點是邊界。領(lǐng)域的核心思想是將問題逐級細(xì)分來減低業(yè)務(wù)和系統(tǒng)的復(fù)雜度,這也是 DDD 討論的核心。
子域
映射概念:子服務(wù)。
領(lǐng)域可以進(jìn)一步劃分成子領(lǐng)域,即子域。這是處理高度復(fù)雜領(lǐng)域的設(shè)計思想,它試圖分離技術(shù)實現(xiàn)的復(fù)雜性。這個拆分的里面在很多架構(gòu)里都有,比如 C4。
核心域
映射概念:核心服務(wù)。
在領(lǐng)域劃分過程中,會不斷劃分子域,子域按重要程度會被劃分成三類:核心域、通用域、支撐域。
通用域
映射概念:中間件服務(wù)或第三方服務(wù)。
支撐域
映射概念:企業(yè)公共服務(wù)。
統(tǒng)一語言
映射概念:統(tǒng)一概念。
定義上下文的含義。它的價值是可以解決交流障礙,不管你是 RD、PM、QA 等什么角色,讓每個團(tuán)隊使用統(tǒng)一的語言(概念)來交流,甚至可讀性更好的代碼。
通用語言包含屬于和用例場景,并且能直接反應(yīng)在代碼中。
可以在事件風(fēng)暴(開會)中來統(tǒng)一語言,甚至是中英文的映射、業(yè)務(wù)與代碼模型的映射等??梢允褂靡粋€表格來記錄。
限界上下文
映射概念:服務(wù)職責(zé)劃分的邊界。
定義上下文的邊界。領(lǐng)域模型存在邊界之內(nèi)。對于同一個概念,不同上下文會有不同的理解,比如商品,在銷售階段叫商品,在運輸階段就叫貨品。
理論上,限界上下文的邊界就是微服務(wù)的邊界,因此,理解限界上下文在設(shè)計中非常重要。
聚合
映射概念:包。
聚合概念類似于你理解的包的概念,每個包里包含一類實體或者行為,它有助于分散系統(tǒng)復(fù)雜性,也是一種高層次的抽象,可以簡化對領(lǐng)域模型的理解。
拆分的實體不能都放在一個服務(wù)里,這就涉及到了拆分,那么有拆分就有聚合。聚合是為了保證領(lǐng)域內(nèi)對象之間的一致性問題。
在定義聚合的時候,應(yīng)該遵守不變形約束法則:
聚合邊界內(nèi)必須具有哪些信息,如果沒有這些信息就不能稱為一個有效的聚合;
聚合內(nèi)的某些對象的狀態(tài)必須滿足某個業(yè)務(wù)規(guī)則:
一個聚合只有一個聚合根,聚合根是可以獨立存在的,聚合中其他實體或值對象依賴與聚合根。
只有聚合根才能被外部訪問到,聚合根維護(hù)聚合的內(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)識,它存在的目的就是為了表示一個值,通常使用值對象來傳達(dá)數(shù)量的形式來表示。
比如 money,讓它具有 id 顯然是不合理的,你也不可能通過 id 查詢一個 money。
定義值對象要依照具體場景的區(qū)分來看,你甚至可以把 Article 中的 Author 當(dāng)成一個值對象,但一定要清楚,Author 獨立存在的時候是實體,或者要拿 Author 做復(fù)雜的業(yè)務(wù)邏輯,那么 Author 也會升級為聚合根。
四種領(lǐng)域模型
失血模型、貧血模型、充血模型、脹血模型
四種模型示例
- 失血模型
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 中進(jìn)行。
這樣做的優(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進(jìn)行持久化交互
- 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 邏輯也被強(qiáng)行放入 Domain Object ,引起了 Domain Object 模型的不穩(wěn)定;
Domain Object 暴露給 Web 層過多的信息,可能引起意想不到的副作用。
DDD落地的一些思考
最近把一個新項目使用了DDD思想進(jìn)行落地,項目代碼結(jié)構(gòu)如下:
整體感覺還是不錯的,不用文檔,基本也能看清楚業(yè)務(wù)的脈絡(luò)。使用Domain Primitive(DP)和CQRS 設(shè)計接口,接口語義更加清楚,接口參數(shù)的清晰度,業(yè)務(wù)代碼邏輯的清晰度顯著提高,而且方便后期讀寫分離,使用Event Sourcing模式,領(lǐng)域?qū)ο蟮臓顟B(tài)完全是由事件驅(qū)動的,可以最大限度的實現(xiàn)系統(tǒng)的松耦合。
唯一的缺點就是代碼量的膨脹,但是這也是不可避免的。對于大團(tuán)隊來講利大于弊,減少溝通成本,小團(tuán)隊按需嘗試。


2023-03-13 09:31:04




