作者:小傅哥,博客:https://bugstack.cn
大家好,我是技術(shù)UP主小傅哥。
DDD 是什么,這應該是每個想使用 DDD 開發(fā)項目的研發(fā)伙伴,遇到的第一個疑問,只有搞清楚它到底是什么才好上手使用。而 DDD 既不是 MVC 一樣的工程結(jié)構(gòu),也不能直接等同于微服務架構(gòu),更不是一種設計模式。
1. DDD 是什么
那 DDD 是什么呢?來自于維基百科的一段定義:"Domain-driven design (DDD) is a major software design approach. ",DDD 是一種軟件設計方法。也就是說 DDD 是指導我們做軟件工程設計的一種手段,它提供了用切割工程模型的各類技巧,如;領(lǐng)域、界限上下文、實體、值對象、聚合、工廠、倉儲等。通過 DDD 的指導思想,我們可以在前期投入更多的時間,更加合理的規(guī)劃出可持續(xù)迭代的工程設計。
在 DDD 中有一套共識的工程兩階段設計手段,包括;戰(zhàn)略設計、戰(zhàn)術(shù)設計。
戰(zhàn)略設計,主要以應對復雜的業(yè)務需求,通過抽象、分治的過程,合理的拆分為獨立的多個微服務,從而分而治之。與之評價拆分的是否合理,則是在需求開發(fā)上線時候,是否每次都大量操作多個微服務開發(fā)和上線。這樣的戰(zhàn)略設計是一種失敗的微服務單體設計。所以少數(shù)幾個中等規(guī)模的單體應用,周圍環(huán)繞著一個服務生態(tài)系統(tǒng),這更有意義。你實際上并沒有構(gòu)建微服務 @賈斯汀·埃瑟里奇
戰(zhàn)術(shù)設計,在這個范疇下,主要以討論如何基于面向?qū)ο笏季S,運用領(lǐng)域模型來表達業(yè)務概念。通常在不做領(lǐng)域模型設計的架構(gòu),也就是通常映射到 MVC 三層架構(gòu)下,Service + 數(shù)據(jù)模型
的開發(fā)模式,會讓 Service 扁平的、大量的,平鋪出非常復雜的業(yè)務邏輯代碼。再加上行為對象與功能邏輯的分離,貧血模型的開發(fā)方式,讓行為對象的不斷交叉使用,也是讓系統(tǒng)不斷增加復雜度,并到難以維護的根因。所以這一階段要設計每一個可以表達領(lǐng)域概念的模型,并運用實體、聚合、領(lǐng)域服務來承載。
2. DDD 的概念
什么是充血模型,領(lǐng)域內(nèi)都包括什么,實體、聚合、值對象,有什么區(qū)別?這樣一些"為什么"的概念,也是戰(zhàn)術(shù)設計過程中非常重要的知識項。搞清楚它們才能做 DDD 設計。
2.1 充血模型
充血模型,指將對象的屬性信息與行為邏輯聚合到一個類中,常用的手段如在對象內(nèi)提供屬于當前對象的信息校驗
、拼裝緩存Key
、不含服務接口調(diào)用的邏輯處理
等。
這樣的方式可以在使用一個對象時,就順便拿到這個對象的提供的一系列方法信息,所有使用對象的邏輯方法,都不需要自己再次處理同類邏輯。但不要只是把充血模型,僅限于一個類的設計和一個類內(nèi)的方法設計。充血還可以是整個包結(jié)構(gòu),一個包下包括了用于實現(xiàn)此包 Service 服務所需的各類零部件(模型、倉儲、工廠),也可以被看做充血模型。同時我們還會再一個同類的類下,提供對應的內(nèi)部類,如用戶實名,包括了,通信類、實名卡、銀行卡、四要素等。它們都被寫進到一個用戶類下的內(nèi)部子類,這樣在代碼編寫中也會清晰的看到子類的所屬信息,更容易理解代碼邏輯,也便于維護迭代。
2.2 領(lǐng)域模型
領(lǐng)域模型,指特定業(yè)務領(lǐng)域內(nèi),業(yè)務規(guī)則、策略以及業(yè)務流程的抽象和封裝。在設計手段上,通過風暴模型拆分領(lǐng)域模塊,形成界限上下文。最大的區(qū)別在于把原有的眾多 Service + 數(shù)據(jù)模型
的方式,拆分為獨立的有邊界的領(lǐng)域模塊。每個領(lǐng)域內(nèi)創(chuàng)建自身所屬的;領(lǐng)域?qū)ο螅▽嶓w、聚合、值對象)、倉儲服務(DAO 操作)、工廠、端口適配器Port(調(diào)用外部接口的手段)等。
那么,現(xiàn)在這里有幾個概念;領(lǐng)域服務、領(lǐng)域?qū)ο?、倉儲定義、事件消息、端口適配器。我們先來看他們是怎么從貧血模型演變過來的,在細分講解每個概念。
- 在原本的 Service + 貧血的數(shù)據(jù)模型開發(fā)指導下,Service 串聯(lián)調(diào)用每一個功能模塊。這些基礎(chǔ)設施(對象、方法、接口)是被相互掉調(diào)用的。這也是因為貧血模型并沒有面向?qū)ο蟮脑O計,所有的需求開發(fā)只有詳細設計。換到充血模型下,現(xiàn)在我們以一個領(lǐng)域功能為聚合,拆分一個領(lǐng)域內(nèi)所需的 Service 為領(lǐng)域服務,VO、Req、Res 重新設計為領(lǐng)域?qū)ο?,DAO、Redis 等持久化操作為倉儲等。舉例;一套賬戶服務中的,授信認證、開戶、提額降額等,每一個都是一個獨立的領(lǐng)域,在每個獨立的領(lǐng)域內(nèi),創(chuàng)建自身領(lǐng)域所需的各項信息。領(lǐng)域模型還有一個特點,它自身只關(guān)注業(yè)務功能實現(xiàn),不與外部任何接口和服務直連。如;不會直接調(diào)用 DAO 操作庫,也不會調(diào)用緩存操作 Redis,更不會直接引入 RPC 連接其他微服務。而是通過倉庫和端口適配器,定義調(diào)用外部數(shù)據(jù)的含有出入?yún)ο蟮慕涌跇藴?,讓基礎(chǔ)設施層做具體的調(diào)用實現(xiàn)。通過這樣的方式讓領(lǐng)域只關(guān)心業(yè)務實現(xiàn),同時做好防腐。
2.3 實體、聚合、值對象
原本在貧血模型下的開發(fā),常常是不會特別在意一個方法的出入?yún)ο蟮?,也?jīng)常是很多個服務共用一個VO對象作為入?yún)?,只要這個對象能把我需要的屬性信息帶進來就可以了。
但在 DDD 的領(lǐng)域模型設計下,領(lǐng)域?qū)ο蟮脑O計是非常面向?qū)ο蟮?。而且在整個風暴事件的四色建模過程也是在以領(lǐng)域?qū)ο鬄轵?qū)動進行的。
實體、聚合、值對象,三者位于每個領(lǐng)域下的領(lǐng)域?qū)ο髢?nèi),服務于領(lǐng)域內(nèi)的領(lǐng)域服務。三個對象定義具體如下;
實體
是依托于持久化層數(shù)據(jù)以領(lǐng)域服務功能目標為指導設計的領(lǐng)域?qū)ο?。持久化PO對象是原子類對象,不具有業(yè)務語義,而實體對象是具有業(yè)務語義且有唯一標識的對象,跟隨于領(lǐng)域服務方法的全生命周期對象。如;用戶PO持久化對象,會涵蓋,用戶的開戶實體、授信實體、額度實體對象。也包括如商品下單時候的購物車實體對象。這個對象也通常是領(lǐng)域服務方法的入?yún)ο蟆?/p>
概念:實體 = 唯一標識 + 狀態(tài)屬性 + 行為動作(功能),是DDD中的一個基本構(gòu)建塊,它代表了具有唯一標識的領(lǐng)域?qū)ο?。實體不僅僅包含數(shù)據(jù)(狀態(tài)屬性),還包含了相關(guān)的行為(功能),并且它的標識在整個生命周期中保持不變。特征:
唯一標識:實體具有一個可以區(qū)分其他實體的標識符。這個標識符可以是一個ID、一個復合鍵或者是一個自然鍵,關(guān)鍵是它能夠唯一地標識實體實例。領(lǐng)域標識:實體的標識通常來源于業(yè)務領(lǐng)域,例如用戶ID、訂單ID等。這些標識符在業(yè)務上有特定的含義,并且在系統(tǒng)中是唯一的。委派標識:在某些情況下,實體的標識可能是由ORM(對象關(guān)系映射)框架自動生成的,如數(shù)據(jù)庫中的自增主鍵。這種標識符雖然可以唯一標識實體,但它并不直接來源于業(yè)務領(lǐng)域。
用途:
表達業(yè)務概念:實體用于在軟件中表達具體的業(yè)務概念,如用戶、訂單、交易等。通過實體的屬性和行為,可以描述這些業(yè)務對象的特征和能力。封裝業(yè)務邏輯:實體不僅僅承載數(shù)據(jù),還封裝了業(yè)務規(guī)則和邏輯。這些邏輯包括驗證數(shù)據(jù)的有效性、執(zhí)行業(yè)務規(guī)則、計算屬性值等。這樣做的目的是保證業(yè)務邏輯的集中和一致性。保持數(shù)據(jù)一致性:實體負責維護自身的狀態(tài)和數(shù)據(jù)一致性。它確保自己的屬性和關(guān)聯(lián)關(guān)系在任何時候都是正確和完整的,從而避免數(shù)據(jù)的不一致性。
實現(xiàn)手段:
定義實體類:在代碼中定義一個類,該類包含實體的屬性、構(gòu)造函數(shù)、方法等。實現(xiàn)唯一標識:為實體類提供一個唯一標識的屬性,如ID,并確保在實體的生命周期中這個標識保持不變。封裝行為:在實體類中實現(xiàn)業(yè)務邏輯的方法,這些方法可以操作實體的狀態(tài),并執(zhí)行相關(guān)的業(yè)務規(guī)則。使用ORM框架:利用ORM框架將實體映射到數(shù)據(jù)庫表中,這樣可以簡化數(shù)據(jù)持久化的操作。實現(xiàn)領(lǐng)域服務:對于跨實體或跨聚合的操作,可以實現(xiàn)領(lǐng)域服務來處理這些操作,而不是在實體中直接實現(xiàn)。使用領(lǐng)域事件:當實體的狀態(tài)發(fā)生變化時,可以發(fā)布領(lǐng)域事件,這樣可以通知其他部分的系統(tǒng)進行相應的處理。
值對象
這個對象在領(lǐng)域服務方法的生命周期過程內(nèi)是不可變對象,也沒有唯一標識。它通常是配合實體對象使用。如為實體對象提供對象屬性值的描述,比如;一個公司雇員的級別值對象,一個下單的商品收貨的四級地址信息對象。所以在開發(fā)值對象的時候,通常不會提供 setter 方法,而是提供構(gòu)造函數(shù)或者 Builder 方法來實例化對象。這個對象通常不會獨立作為方法的入?yún)ο?,但做可以獨立作為出參對象使用?/p>
概念:值對象是由一組屬性組成的,它們共同描述了一個領(lǐng)域概念。與實體(Entity)不同,值對象不需要有一個唯一的標識符來區(qū)分它們。值對象通常是不可變的,這意味著一旦創(chuàng)建,它們的狀態(tài)就不應該改變。特征:
不可變性(Immutability):值對象一旦被創(chuàng)建,它的狀態(tài)就不應該發(fā)生變化。這有助于保證領(lǐng)域模型的一致性和線程安全性。等價性(Equality):值對象的等價性不是基于身份或引用,而是基于對象的屬性值。如果兩個值對象的所有屬性值都相等,那么這兩個對象就被認為是等價的。替換性(Replaceability):由于值對象是不可變的,任何需要改變值對象的操作都會導致創(chuàng)建一個新的值對象實例,而不是修改現(xiàn)有的實例。側(cè)重于描述事物的狀態(tài):值對象通常用來描述事物的狀態(tài),而不是事物的唯一身份??蓮陀眯裕≧eusability):值對象可以在不同的領(lǐng)域?qū)嶓w或其他值對象中重復使用。
用途:
金額和貨幣(如價格、工資、費用等)度量和數(shù)據(jù)(如重量、長度、體積等)范圍或區(qū)間(如日期范圍、溫度區(qū)間等)復雜的數(shù)學模型(如坐標、向量等)任何其他需要封裝的屬性集合
實現(xiàn)手段:
定義不可變類:確保類的所有屬性都是私有的,并且只能通過構(gòu)造函數(shù)來設置。重寫equals和hashCode方法:這樣可以確保值對象的等價性是基于它們的屬性值,而不是對象的引用。提供只讀訪問器:只提供獲取屬性值的方法,不提供修改屬性值的方法。使用工廠方法或構(gòu)造函數(shù)創(chuàng)建實例:這有助于確保值對象的有效性和一致性??紤]序列化支持:如果值對象需要在網(wǎng)絡上傳輸或存儲到數(shù)據(jù)庫中,需要提供序列化和反序列化的支持。
聚合
當你對數(shù)據(jù)庫的操作需要使用到多個實體時,可以創(chuàng)建聚合對象。一個聚合對象,代表著一個數(shù)據(jù)庫事務,具有事務一致性。聚合中的實體可以由聚合提供創(chuàng)建操作,實體也被稱為聚合根對象。一個訂單的聚合,會涵蓋;下單用戶實體對象、訂單實體、訂單明細實體和訂單收貨四級地址值對象。而那個作為入?yún)⒌馁徫镘噷嶓w對象,已經(jīng)被轉(zhuǎn)換為實體對象了?!?聚合內(nèi)事務一致性,聚合外最終一致性。
-
- 概念:聚合是領(lǐng)域模型中的一個關(guān)鍵概念,它是一組具有內(nèi)聚性的相關(guān)對象的集合,這些對象一起工作以執(zhí)行某些業(yè)務規(guī)則或操作。聚合定義了一組對象的邊界,這些對象可以被視為一個單一的單元進行處理。特征:
-
- 一致性邊界:聚合確保其內(nèi)部對象的狀態(tài)變化是一致的。當對聚合內(nèi)的對象進行操作時,這些操作必須保持聚合內(nèi)所有對象的一致性。根實體:每個聚合都有一個根實體(Aggregate Root),它是聚合的入口點。根實體擁有一個全局唯一的標識符,其他對象通過根實體與聚合交互。事務邊界:聚合也定義了事務的邊界。在聚合內(nèi)部,所有的變更操作應該是原子的,即它們要么全部成功,要么全部失敗,以此來保證數(shù)據(jù)的一致性。
-
用途:
-
-
- 封裝業(yè)務邏輯:聚合通過將相關(guān)的對象和操作封裝在一起,提供了一個清晰的業(yè)務邏輯模型,有助于業(yè)務規(guī)則的實施和維護。保證一致性:聚合確保內(nèi)部狀態(tài)的一致性,通過定義清晰的邊界和規(guī)則,聚合可以在內(nèi)部強制執(zhí)行業(yè)務規(guī)則,從而保證數(shù)據(jù)的一致性。簡化復雜性:聚合通過組織相關(guān)的對象,簡化了領(lǐng)域模型的復雜性。這有助于開發(fā)者更好地理解和擴展系統(tǒng)。
-
實現(xiàn)手段:
-
- 定義聚合根:選擇合適的聚合根是實現(xiàn)聚合的第一步。聚合根應該是能夠代表整個聚合的實體,并且擁有唯一標識。限制訪問路徑:只能通過聚合根來修改聚合內(nèi)的對象,不允許直接修改聚合內(nèi)部對象的狀態(tài),以此來維護邊界和一致性。設計事務策略:在聚合內(nèi)部實現(xiàn)事務一致性,確保操作要么全部完成,要么全部回滾。對于聚合之間的交互,可以采用領(lǐng)域事件或其他機制來實現(xiàn)最終一致性。封裝業(yè)務規(guī)則:在聚合內(nèi)部實現(xiàn)業(yè)務規(guī)則和邏輯,確保所有的業(yè)務操作都遵循這些規(guī)則。持久化:聚合根通常與數(shù)據(jù)持久化層交互,以保存聚合的狀態(tài)。這通常涉及到對象-關(guān)系映射(ORM)或其他數(shù)據(jù)映射技術(shù)。
2.4 倉儲和適配器
在 DDD 的設計方法中,領(lǐng)域?qū)幼龅搅酥魂P(guān)心領(lǐng)域服務實現(xiàn)。最能體現(xiàn)這樣設計的就是倉庫和適配器的設計。通常在 Service + 數(shù)據(jù)模型
的設計中,會在 Service 中引入 Redis、RPC、配置中心等各類其他外部服務。但在 DDD 中,通過倉儲和適配器以及基礎(chǔ)設施層的定義,解耦了這部分內(nèi)容。
特征:
-
-
- 封裝持久化操作:Repository負責封裝所有與數(shù)據(jù)源交互的操作,如創(chuàng)建、讀取、更新和刪除(CRUD)操作。這樣,領(lǐng)域?qū)拥拇a就可以避免直接處理數(shù)據(jù)庫或其他存儲機制的復雜性。領(lǐng)域?qū)ο蟮募瞎芾恚篟epository通常被視為領(lǐng)域?qū)ο蟮募?,提供了查詢和過濾這些對象的方法,使得領(lǐng)域?qū)ο蟮墨@取和管理更加方便。抽象接口:Repository定義了一個與持久化機制無關(guān)的接口,這使得領(lǐng)域?qū)拥拇a可以在不同的持久化機制之間切換,而不需要修改業(yè)務邏輯。
-
用途:
-
-
- 數(shù)據(jù)訪問抽象:Repository為領(lǐng)域?qū)犹峁┝艘粋€清晰的數(shù)據(jù)訪問接口,使得領(lǐng)域?qū)ο罂梢詫W⒂跇I(yè)務邏輯的實現(xiàn),而不是數(shù)據(jù)訪問的細節(jié)。領(lǐng)域?qū)ο蟮牟樵兒凸芾恚篟epository使得對領(lǐng)域?qū)ο蟮牟樵兒凸芾碜兊酶臃奖愫挽`活,支持復雜的查詢邏輯。領(lǐng)域邏輯與數(shù)據(jù)存儲分離:通過Repository模式,領(lǐng)域邏輯與數(shù)據(jù)存儲邏輯分離,提高了領(lǐng)域模型的純粹性和可測試性。優(yōu)化數(shù)據(jù)訪問:Repository實現(xiàn)可以包含數(shù)據(jù)訪問的優(yōu)化策略,如緩存、批處理操作等,以提高應用程序的性能。
-
實現(xiàn)手段:
-
- 定義Repository接口:在領(lǐng)域?qū)佣x一個或多個Repository接口,這些接口聲明了所需的數(shù)據(jù)訪問方法。實現(xiàn)Repository接口:在基礎(chǔ)設施層或數(shù)據(jù)訪問層實現(xiàn)這些接口,具體實現(xiàn)可能是使用ORM(對象關(guān)系映射)框架,如MyBatis、Hibernate等,或者直接使用數(shù)據(jù)庫訪問API,如JDBC等。依賴注入:在應用程序中使用依賴注入(DI)來將具體的Repository實現(xiàn)注入到需要它們的領(lǐng)域服務或應用服務中。這樣做可以進一步解耦領(lǐng)域?qū)雍蛿?shù)據(jù)訪問層,同時也便于單元測試。使用規(guī)范模式(Specification Pattern):有時候,為了構(gòu)建復雜的查詢,可以結(jié)合使用規(guī)范模式,這是一種允許將業(yè)務規(guī)則封裝為單獨的業(yè)務邏輯單元的模式,這些單元可以被Repository用來構(gòu)建查詢。
Repository模式是DDD(領(lǐng)域驅(qū)動設計)中的一個核心概念,它有助于保持領(lǐng)域模型的聚焦和清晰,同時提供了靈活、可測試和可維護的數(shù)據(jù)訪問策略。
倉儲解耦的手段使用了依賴倒置的設計,所有領(lǐng)域需要的外部服務,不在直接引入外部的服務,而是通過定義接口的方式,讓基礎(chǔ)設施層實現(xiàn)領(lǐng)域?qū)咏涌冢▊}儲/適配器)的方式來處理。
那么也就是基礎(chǔ)設置層負責原則對接數(shù)據(jù)庫
、緩存
、配置中心
、RPC接口
、HTTP接口
、MQ推送
等各項資源,并承接領(lǐng)域服務的接口調(diào)用各項服務為領(lǐng)域?qū)犹峁?shù)據(jù)能力。
同時這也會體現(xiàn)出,領(lǐng)域?qū)拥膶崿F(xiàn)是具有業(yè)務語義的,而到了基礎(chǔ)設置層則沒有業(yè)務語義,都是原子的方法。通過原子方法的組合為領(lǐng)域業(yè)務語義提供支撐。
2.5 領(lǐng)域編排
在 DDD 中,每一個領(lǐng)域都是界限上下文拆分的獨立結(jié)果,而實現(xiàn)業(yè)務流程的功能則需要串聯(lián)各個領(lǐng)域模塊提供一整條鏈路的完整服務。所以也常說領(lǐng)域內(nèi)事務一致性,領(lǐng)域外最終一致性。
同時這些領(lǐng)域模塊因為是獨立的,所以也可以被復用。在不同的場景功能訴求下,可以選擇不同的領(lǐng)域模塊進行組裝,這個過程就像搭積木一樣。
但這里有一個取舍,如果項目相對來說并不大,也沒有太多的編排處理。那么可以直接讓觸發(fā)器層對接領(lǐng)域?qū)?,減少編排層后,編碼會更加便捷。
2.6 觸發(fā)器
在所有的模型都定義完成后,領(lǐng)域業(yè)務被串聯(lián)了。那么接下來則是使用,而使用的方式可以包括;接口(http/rpc)、消息監(jiān)聽、定時任務等方式,這些方式統(tǒng)一被定義為觸發(fā)動作。
由觸發(fā)發(fā)起對編排功能的調(diào)用處理。如;定時任務做信貸的計息、開戶成功消息通知返利優(yōu)惠券、提供接口讓外部調(diào)用授信邏輯等。這些都是觸發(fā)動作。
3. 推薦閱讀
- DDD 實戰(zhàn)項目,大營銷平臺系統(tǒng)DDD 工程腳手架MVC2DDD 重構(gòu)經(jīng)驗總結(jié)DDD 四色建模
加入學習
星球「碼農(nóng)會鎖」??實戰(zhàn)項目中有非常多的運用。還包括:大營銷、OpenAI 應用、API網(wǎng)關(guān)、Lottery抽獎、IM通信、SpringBoot Starter 組件開發(fā)、IDEA Plugin 插件開發(fā)、支付SDK、動態(tài)線程組件、透視業(yè)務監(jiān)控等,并還有開源項目學習。
如果大家希望通過做有價值的編程項目,提高自己的編程思維和編碼能力,可以加入小傅哥的【星球:碼農(nóng)會鎖】。加入后解鎖所有往期項目,還可以學習后續(xù)新開發(fā)的項目。
這樣成體系的全量項目學習,放在一些平臺售賣,至少都要上千塊。但小傅哥的星球,只需要100多,就可以獲得大廠架構(gòu)師對你手把手教學!