生活随笔
收集整理的這篇文章主要介紹了
DDD关键知识点整理汇总
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
創(chuàng)建領(lǐng)域?qū)ο蟛捎脴?gòu)造函數(shù)或者工廠,如果用工廠時(shí)需要依賴于領(lǐng)域服務(wù)或倉儲(chǔ),則通過構(gòu)造函數(shù)注入到工廠; 一個(gè)聚合是由一些列相聯(lián)的 Entity 和 Value Object 組成,一個(gè)聚合有一個(gè)聚合根,聚合根是 Entity ,整個(gè)聚合被看成是一個(gè)數(shù)據(jù)修改的單元,也就是說整個(gè)聚合內(nèi)的所有對象要么同時(shí)被保存,要么都不能保存,即保存到數(shù)據(jù)持久層時(shí)必須以覆蓋的方式來保存,而不是追加方式或合并的方式來保存,否則無法確保聚合內(nèi)的對象的數(shù)據(jù)一致性。另外,整個(gè)聚合的不變性約束由聚合根負(fù)責(zé)維護(hù)。作為推導(dǎo)的一個(gè)結(jié)論:我們不能只保存一個(gè)聚合內(nèi)的一部分對象;聚合內(nèi)的所有實(shí)體和值對象應(yīng)該總是一起被取出來一起被保存,因?yàn)橐粋€(gè)聚合是一個(gè)數(shù)據(jù)持久化的單元,不需要考慮將整個(gè)聚合根取出來有性能問題,因?yàn)槿魏我粋€(gè)聚合根都有明確的邊界。目前的內(nèi)存緩存框架都已發(fā)展的比較成熟,性能已經(jīng)不是問題;如 MongoDb , MemCache , NoSQL ,等等; 聚合內(nèi)的對象之所以聚合在一起的關(guān)鍵原因不是因?yàn)樗鼈兙哂幸恍╆P(guān)聯(lián)關(guān)系或依賴關(guān)系,而是因?yàn)榫酆蟽?nèi)的對象之間具有某些不變性規(guī)則,在任何時(shí)候,聚合內(nèi)的所有這些對象必須滿足這些不變性規(guī)則。所以,如果一些對象之間看似有一些關(guān)聯(lián)關(guān)系或依賴關(guān)系,但是他們之間不具有任何不變性約束,那么就不應(yīng)該把這些對象放在一個(gè)聚合中,否則只會(huì)增加這些對象之間不必要的耦合性,增加對象維護(hù)的難度; (Remembering that aggregates are not about composition, but about managing invariants, we don't compose entities on an aggregate root only as a matter of convenience) 。那么為什么一些對象之間有不變性約束后就一定非要聚合在一起不可呢?首先需要先明確一下什么是聚合,聚合是一個(gè)整體,是修改數(shù)據(jù)的一個(gè)最小單元,一個(gè)聚合有一個(gè)頭,即聚合根,聚合根維護(hù)了整個(gè)聚合的不變性,所以整個(gè)聚合在外面看來就是一個(gè)對象,而不是多個(gè)對象的組合。另外一點(diǎn)非常重要,聚合在被持久化到數(shù)據(jù)庫時(shí),是以完全覆蓋的且事務(wù)的方式保存。好了有了前面的共識(shí)之后,我們再想想為什么聚合能保證多個(gè)對象之間的不變性規(guī)則約束?其實(shí)很只要真正理解了前面的約束之后就很容易理解了。你想想不管一個(gè)聚合中有什么約束,所有的約束由該聚合自己維護(hù),所以就可以確保數(shù)據(jù)在領(lǐng)域模型級別就是完全一致的,沒有任何違反規(guī)則的錯(cuò)誤數(shù)據(jù),即內(nèi)存中的數(shù)據(jù)都是正確的。再加上這些正確的數(shù)據(jù)被持久化時(shí)是以完全覆蓋的且事務(wù)的方式保存,從而也確保了數(shù)據(jù)庫里的數(shù)據(jù)不可能出現(xiàn)不一致。這里唯一讓你可能擔(dān)心的問題是,如果多個(gè)用戶同時(shí)更新一個(gè)聚合時(shí),會(huì)產(chǎn)生并發(fā)沖突,此時(shí)將會(huì)使系統(tǒng)變得不可用!其實(shí)我認(rèn)為這不是個(gè)問題,因?yàn)楝F(xiàn)在的支持高并發(fā)寫的分布式存儲(chǔ)數(shù)據(jù)庫已經(jīng)非常成熟,比如淘寶的 oceanbase (已經(jīng)開源了) , 還有那些 NoSQL 也支持,或者用分布式緩存或 MongoDB 也效率不錯(cuò)。就算沒這么好的存儲(chǔ)機(jī)制支持,用傳統(tǒng)的數(shù)據(jù)庫來存儲(chǔ),我相信也不會(huì)有大問題,現(xiàn)在的數(shù)據(jù)庫已經(jīng)不是 10 年前的數(shù)據(jù)庫了,在處理高并發(fā)寫的能力上已經(jīng)不是同日而語了。其實(shí)并發(fā)沖突并沒有你想的那么嚴(yán)重,一般通過 select before update ,以及 version 樂觀鎖定,就沒問題了。支付寶一天幾千萬比在線交易,全部是強(qiáng)一致性,不然不叫在線交易系統(tǒng)。聚合根的存儲(chǔ)屬于單點(diǎn)存儲(chǔ),不能用最終一致性。最終一致性是弱一致性的一種特殊方式,但是最終一致性往往用于處理分布式系統(tǒng)中同一份數(shù)據(jù)在多個(gè)地方有備份,然后可能會(huì)出現(xiàn)多個(gè)地方數(shù)據(jù)不一致的問題,但是最終都會(huì)一致即同步完成。具體大家可以看看 CAP 定理。 所謂的不變性約束是指:假設(shè)有一個(gè)采購訂單 Order ,一個(gè) Order 下有多個(gè)訂單項(xiàng) OrderItem ,假設(shè)有一個(gè)約束是,該采購訂單的總額不能超過 100 元。那么訂單的總額不能超過 100 元就是一個(gè)不變性約束;那么 Order 和 OrderItem 聚合在一起就顯得很有意義。在這種情況下,有 Order 來維護(hù)這個(gè)規(guī)則,當(dāng)整個(gè)訂單被保存時(shí),比如采用覆蓋的方式保存到數(shù)據(jù)庫。再舉個(gè)例子,比如一個(gè)論壇中有帖子和回復(fù),大家都知道一個(gè)帖子有多個(gè)回復(fù),回復(fù)離開帖子沒有意義。所以大家很自然會(huì)認(rèn)為帖子和回復(fù)應(yīng)該在一個(gè)聚合內(nèi),帖子是聚合根。但是這樣其實(shí)很有問題,仔細(xì)想想會(huì)發(fā)現(xiàn)帖子和回復(fù)之間并沒有不變性約束規(guī)則,回復(fù)和帖子之間只有一個(gè)簡單的 1:N 的關(guān)系而已。如果每次在添加一個(gè)回復(fù)時(shí),都把帖子先取出來,然后在帖子的回復(fù)列表中把新的回復(fù)添加進(jìn)去,然后再保存整個(gè)帖子,那么不難想象,這樣做無疑是小題大做,并且每次為了更新一個(gè)回復(fù)或新增一個(gè)回復(fù),就要把整個(gè)帖子取出來,這樣做無疑非常浪費(fèi)內(nèi)存,并且在多用戶并發(fā)回同一個(gè)帖子的情況下則會(huì)更糟糕。實(shí)際上仔細(xì)分析一下,帖子和回復(fù)都應(yīng)該是聚合,并且分別都是聚合根,我們要確保的僅僅是回復(fù)的帖子不能被修改即可。添加一個(gè)回復(fù)實(shí)際上和帖子無關(guān),帖子根本不關(guān)心已經(jīng)有多少個(gè)回復(fù)了。這點(diǎn)和之前的訂單的例子不同,訂單需要準(zhǔn)確維護(hù)其包含的所有訂單項(xiàng)以便能夠計(jì)算出總價(jià)是否超出 100 元。其實(shí)這么多問題還是不足以詳細(xì)說明什么樣的對象該被聚合在一起,這里只是作為拋磚引玉,引發(fā)大家思考如何設(shè)計(jì)聚合。 一個(gè)聚合需要具備哪些更多的特征呢? 1 )需要具備前面說的基本特征; 2 )聚合內(nèi)的子對象要么是值對象,要么是只讀的實(shí)體,為什么需要只讀,因?yàn)榫酆系淖訉?shí)體是可以被臨時(shí)傳遞到外部的,要是外面的對象調(diào)用子對象的某個(gè)方法修改了子對象的屬性,那么就意味著繞過聚合根修改了聚合內(nèi)的東西,這樣就無法確保聚合內(nèi)的不變性了; 3 )如果聚合根有集合類型的屬性,那么該集合也必須是只讀的,即不允許別人在外部添加或刪除集合的元素,否則也同樣無法確保聚合的不變性。總之,我們要避免任何可能從外部修改聚合的行為發(fā)生,所有修改聚合的行為必須通過聚合根來實(shí)現(xiàn)。所以,理論上我們推薦大家在聚合內(nèi)盡量設(shè)計(jì)值對象,原因大家多想想吧!其實(shí)從邏輯哲學(xué)的角度去思考,值對象表示了不變性,值對象表示一個(gè)值,值可以用來描述事物,事物就是實(shí)體。要是實(shí)體是由其他實(shí)體來描述,而其它實(shí)體是可變的,那么如何確保被描述的實(shí)體是可控的?大家想想為什么 DDD 書中,為什么要在 OrderItem 中存放當(dāng)時(shí)購買時(shí)的 Price 就知道了。要是直接引用 Product 對象,那么會(huì)導(dǎo)致 OrderItem 引用了一個(gè)可變的對象,就無法確保訂單的不變性約束。而唯有持久一個(gè)不變的值對象,才能維持其不變性。 Evans 關(guān)于聚合的兩條推薦準(zhǔn)則: 1 )聚合不要設(shè)計(jì)的過大,過大的聚合很難確保不變性,從而很難確保數(shù)據(jù)的強(qiáng)一致性; 2 )聚合與聚合之間不要通過引用的方式來關(guān)聯(lián),而應(yīng)該通過 ID 關(guān)聯(lián),通過 ID 關(guān)聯(lián)也同樣能表示聚合之間的關(guān)系,并且具有更好的性能和可伸縮性,聚合根之間通過 ID 關(guān)聯(lián)的好處是:不會(huì)因?yàn)?/span>Load 一個(gè)聚合根而把其他關(guān)聯(lián)的聚合根一起 Load 出來,這樣也避免了 Load 一個(gè)聚合根會(huì)把整個(gè)數(shù)據(jù)庫 Load 出來的風(fēng)險(xiǎn);另外,對 ORM 的要求也很低,不需要 ORM 支持 LazyLoad ;聚合根與聚合根之間的關(guān)系不像聚合內(nèi)的 Entity 之間這么強(qiáng)烈內(nèi)聚,它們之間僅僅是某種比較弱的關(guān)聯(lián)關(guān)系,每個(gè)聚合根都有其獨(dú)立的生命周期; 聚合內(nèi)的非跟的 Entity 以及 Value Object 之間不要相互引用,聚合內(nèi)的所有 Child 可以對根 Entity 持有引用,如果一個(gè) Child Entity 需要和另外一個(gè) Child Entity 交互,則因該通過聚合根完成; 我們應(yīng)該盡量減少聚合之間關(guān)聯(lián),盡量做到單向關(guān)聯(lián),只保留確實(shí)需要處理的經(jīng)常需要用到的遍歷方向的關(guān)聯(lián); 倉儲(chǔ)應(yīng)理解為一個(gè)在內(nèi)存中維護(hù)一系列聚合根的集合; 一個(gè)聚合根配備一個(gè)倉儲(chǔ); 倉儲(chǔ)提供的接口應(yīng)該總是接受聚合根或返回聚合根,不能返回聚合內(nèi)的其他 Entity 或 Value Object ; 不要把倉儲(chǔ)理解為 DAO ,倉儲(chǔ)屬于領(lǐng)域模型的一部分,代表了領(lǐng)域模型向外提供接口的一部分,而 DAO 是表示數(shù)據(jù)庫向上層提供的接口表示; 倉儲(chǔ)的目的不是為了支持界面查詢,不要給倉儲(chǔ)中設(shè)計(jì)一些目的是為了為界面提供顯示數(shù)據(jù)的接口,倉儲(chǔ)提供的所有接口應(yīng)該僅為領(lǐng)域模型使用;基本的倉儲(chǔ)接口只需要三個(gè): Add , Remove , GetById ,其他的擴(kuò)展接口可以根據(jù)業(yè)務(wù)需要擴(kuò)展接口聲明; 如果一個(gè)操作僅由一個(gè)聚合根就可以完成,那么直接調(diào)用該聚合根完成即可; 領(lǐng)域服務(wù)表示領(lǐng)域模型中的一些業(yè)務(wù)操作,這些操作通常由多個(gè)聚合根或倉儲(chǔ)或其他領(lǐng)域服務(wù)相互協(xié)作完成,那么需要為這些操作建立領(lǐng)域服務(wù),在領(lǐng)域服務(wù)中以過程化的方式來一步步首先根據(jù)各個(gè)聚合的 ID 獲取到操作的相關(guān)聚合根,然后調(diào)用聚合根完成整個(gè)業(yè)務(wù)操作;比如資金轉(zhuǎn)帳,這是經(jīng)典的領(lǐng)域服務(wù)的例子;再比如在調(diào)用某個(gè)聚合根做一個(gè)數(shù)據(jù)更新之前需要先判斷一些業(yè)務(wù)規(guī)則,但是這些判斷規(guī)則不能在該聚合根內(nèi)做,因?yàn)檫@樣做可能會(huì)導(dǎo)致聚合根依賴于外部的領(lǐng)域服務(wù)或倉儲(chǔ),此時(shí),應(yīng)該交給領(lǐng)域服務(wù)來完成規(guī)則校驗(yàn)和聚合根數(shù)據(jù)更新的整個(gè)過程。領(lǐng)域服務(wù)可以依賴倉儲(chǔ)或聚合根; 領(lǐng)域服務(wù)依賴倉儲(chǔ)時(shí),工廠依賴于領(lǐng)域服務(wù)或倉儲(chǔ)時(shí),都因該采用構(gòu)造函數(shù)注入的方式,這樣可以避免領(lǐng)域模型中不會(huì)出現(xiàn) DependencyResolver.Resolve<T>() 這樣的語句; 切忌不要因?yàn)轭I(lǐng)域服務(wù)的引入讓聚合根變得貧血,聚合根應(yīng)該有的職責(zé)還是必須要由聚合根來承擔(dān); 聚合根內(nèi)不要依賴領(lǐng)域服務(wù)或倉儲(chǔ),如果你發(fā)現(xiàn)一個(gè)聚合根的職責(zé)需要依賴于某個(gè)領(lǐng)域服務(wù)或倉儲(chǔ)來幫忙完成一些其他的邏輯(像判斷業(yè)務(wù)規(guī)則之類),那么通常你要考慮這個(gè)職責(zé)不應(yīng)該由該聚合根來承擔(dān),而應(yīng)該建立合適的領(lǐng)域服務(wù)來承擔(dān);聚合根的主要職責(zé)是管理其內(nèi)聚的所有 Child Entity 或 Value Object 的業(yè)務(wù)完整性; 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)時(shí),為對象分配職責(zé)時(shí),可以參考信息專家模式:將職責(zé)分配給擁有執(zhí)行該職責(zé)所需信息的人;如果一個(gè)聚合根看起來擁有執(zhí)行某個(gè)職責(zé)所需的信息,但沒包含全部所需信息,此時(shí)則不應(yīng)該將該職責(zé)分配給該聚合根,因?yàn)閺?qiáng)行分配給它,會(huì)導(dǎo)致該聚合根沒有內(nèi)聚性,因?yàn)閯荼貢?huì)依賴于其它的領(lǐng)域?qū)ο蠡蝾I(lǐng)域服務(wù)或倉儲(chǔ); 要學(xué)習(xí) CQRS 架構(gòu),要知道我們應(yīng)該將應(yīng)用程序的業(yè)務(wù)邏輯處理部分(即用戶命令響應(yīng)部分)和查詢部分分離;我們應(yīng)該用兩個(gè)不同的技術(shù)來實(shí)現(xiàn)這兩個(gè)部分的實(shí)現(xiàn);用 DDD 領(lǐng)域模型來實(shí)現(xiàn)命令部分;用最快的查詢引擎來實(shí)現(xiàn)查詢部分; 如果要采用 CQRS 架構(gòu),我們需要考慮一個(gè)成熟可靠的底層框架,否則很容易導(dǎo)致命令端產(chǎn)生的領(lǐng)域?qū)ο蟮臓顟B(tài)無法同步(后者丟失)到查詢端的存儲(chǔ)中; 領(lǐng)域?qū)ο笊系膶傩钥梢跃哂?/span>get 和 set ,因?yàn)槲覀兤綍r(shí)所理解的對象不是真正的對象,而是某個(gè)事實(shí)的描述,比如圖書管理系統(tǒng)中的一個(gè) Book 對象,表示圖書管中放著一本書,然后該書可能有一個(gè)入庫時(shí)間。現(xiàn)實(shí)生活正的話,書本的入庫時(shí)間絕對不可能變化,但是軟件中的 Book 因?yàn)椴皇钦嬲默F(xiàn)實(shí)生活中的書本,而只是表示圖書館中有一本書這個(gè)事實(shí)的描述,我們當(dāng)然可以修改這個(gè)事實(shí),因?yàn)槲覀兛赡芤驗(yàn)橹霸跁救霂鞎r(shí)所輸入的入庫時(shí)間是錯(cuò)的,需要修改該入庫時(shí)間,此時(shí)就有提供 set 的必要了。所以,理論上任何一個(gè) Entity ,除了 ID 之外,其他所有屬性都可以更改,因?yàn)檫@些屬性并不表示現(xiàn)實(shí)生活中的真正對象的特征,而僅僅只是對一個(gè)事實(shí)的描述;剛開始 Book 對象對書本入庫這個(gè)事實(shí)的描述可能有問題,此時(shí)我們就需要修改該 Book 的屬性;我想這個(gè)例子已經(jīng)充分說明為什么可以提供 get 和 set 了; 不要總是零散的不加任何分組的設(shè)計(jì) Entity 的屬性,因?yàn)橛行傩栽谶壿嬌匣驑I(yè)務(wù)上就是內(nèi)聚的,代表一個(gè)完整的概念,比如 Country,Province,City,Town,Street ,等這些屬性表示一個(gè)地址的信息,此時(shí)我們應(yīng)該設(shè)計(jì)一個(gè) Address 對象來表示該地址信息,此時(shí)該 Address 就是一個(gè)值對象。所以我們在設(shè)計(jì) Entity 的屬性時(shí),要好好想想,哪些子屬性其實(shí)在業(yè)務(wù)上是一個(gè)完整的概念,此時(shí)我們就需要考慮將這些相關(guān)的屬性設(shè)計(jì)為一個(gè)值對象; 切忌值對象必須是只讀的,值對象之所以叫值對象最主要的是因?yàn)樗硎疽粋€(gè)值,而不是一個(gè)對象;值是不會(huì)變化的,是一個(gè)明確含義的不變的事物,比如 3 表示一個(gè)值,表述數(shù)量是 3 , 3 永遠(yuǎn)不能變化;所以說,世界之所以存在,是因?yàn)橛羞@些永恒不變的值對象的存在;我們只要把值對象理解為 3 ,“ abcd ”這樣的永恒不變的值就行了; 不要讓領(lǐng)域模型去模擬現(xiàn)實(shí),模擬用戶(軟件使用者)與領(lǐng)域模型交互的過程;領(lǐng)域模型要實(shí)現(xiàn)的應(yīng)該是用戶的需求,領(lǐng)域模型中不應(yīng)該包含用戶的成分,想想只有空杯子才能裝水的道理,即無為以之用的道理就明白了;所以,我們在設(shè)計(jì)領(lǐng)域模型時(shí)首先要明白領(lǐng)域模型要完成的事情是什么;這方面,多看看用例圖,就知道軟件該做的事情了,推薦大家看的書是: Craig Larman 寫的《 UML 和模式應(yīng)用》一書,非常經(jīng)典; DDD中強(qiáng)調(diào)“領(lǐng)域?qū)ο笫菗碛行袨榈摹薄_@句話我覺得說法是正確的,但是其做法難道就是“在領(lǐng)域?qū)ο罄飳懛椒ā边@么簡單嗎? 我們常說“類應(yīng)該具有生命的”,但我不認(rèn)為“把方法寫到類里就會(huì)讓類具有生命了”,因?yàn)?#34;把簡單地把方法寫到類里,其最終也只是讓類變成了一潭死水,是經(jīng)不起風(fēng)浪的,是無法變成湖泊和海洋的”。類不會(huì)無緣無故產(chǎn)生行為,類能夠產(chǎn)生行為一定是在一定的場景下發(fā)生的。在我看來,“簡單地把方法寫進(jìn)類里是無法描述多(復(fù)雜)場景下的類的行為的”。如果有人說在他的項(xiàng)目里那樣做沒問題,那我只能說他的項(xiàng)目(場景)還不夠復(fù)雜。 其實(shí)“貧血對象”和“充血對象”都是極端的做法,而問題的關(guān)鍵是“類如何合理而自然地?fù)碛行袨椤薄T谖铱磥?#xff0c;我們只能在“貧血對象”和“充血對象”之間達(dá)成一種平衡。如何把“場景”更好地融入 DDD 還有沒公論,但我想“類合理而自然地?fù)碛行袨椤睉?yīng)該是一條準(zhǔn)則。
與50位技術(shù)專家面對面 20年技術(shù)見證,附贈(zèng)技術(shù)全景圖
總結(jié)
以上是生活随笔 為你收集整理的DDD关键知识点整理汇总 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔 推薦給好友。