生活随笔
收集整理的這篇文章主要介紹了
DDD关键知识点整理汇总
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
創建領域對象采用構造函數或者工廠,如果用工廠時需要依賴于領域服務或倉儲,則通過構造函數注入到工廠; 一個聚合是由一些列相聯的 Entity 和 Value Object 組成,一個聚合有一個聚合根,聚合根是 Entity ,整個聚合被看成是一個數據修改的單元,也就是說整個聚合內的所有對象要么同時被保存,要么都不能保存,即保存到數據持久層時必須以覆蓋的方式來保存,而不是追加方式或合并的方式來保存,否則無法確保聚合內的對象的數據一致性。另外,整個聚合的不變性約束由聚合根負責維護。作為推導的一個結論:我們不能只保存一個聚合內的一部分對象;聚合內的所有實體和值對象應該總是一起被取出來一起被保存,因為一個聚合是一個數據持久化的單元,不需要考慮將整個聚合根取出來有性能問題,因為任何一個聚合根都有明確的邊界。目前的內存緩存框架都已發展的比較成熟,性能已經不是問題;如 MongoDb , MemCache , NoSQL ,等等; 聚合內的對象之所以聚合在一起的關鍵原因不是因為它們具有一些關聯關系或依賴關系,而是因為聚合內的對象之間具有某些不變性規則,在任何時候,聚合內的所有這些對象必須滿足這些不變性規則。所以,如果一些對象之間看似有一些關聯關系或依賴關系,但是他們之間不具有任何不變性約束,那么就不應該把這些對象放在一個聚合中,否則只會增加這些對象之間不必要的耦合性,增加對象維護的難度; (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) 。那么為什么一些對象之間有不變性約束后就一定非要聚合在一起不可呢?首先需要先明確一下什么是聚合,聚合是一個整體,是修改數據的一個最小單元,一個聚合有一個頭,即聚合根,聚合根維護了整個聚合的不變性,所以整個聚合在外面看來就是一個對象,而不是多個對象的組合。另外一點非常重要,聚合在被持久化到數據庫時,是以完全覆蓋的且事務的方式保存。好了有了前面的共識之后,我們再想想為什么聚合能保證多個對象之間的不變性規則約束?其實很只要真正理解了前面的約束之后就很容易理解了。你想想不管一個聚合中有什么約束,所有的約束由該聚合自己維護,所以就可以確保數據在領域模型級別就是完全一致的,沒有任何違反規則的錯誤數據,即內存中的數據都是正確的。再加上這些正確的數據被持久化時是以完全覆蓋的且事務的方式保存,從而也確保了數據庫里的數據不可能出現不一致。這里唯一讓你可能擔心的問題是,如果多個用戶同時更新一個聚合時,會產生并發沖突,此時將會使系統變得不可用!其實我認為這不是個問題,因為現在的支持高并發寫的分布式存儲數據庫已經非常成熟,比如淘寶的 oceanbase (已經開源了) , 還有那些 NoSQL 也支持,或者用分布式緩存或 MongoDB 也效率不錯。就算沒這么好的存儲機制支持,用傳統的數據庫來存儲,我相信也不會有大問題,現在的數據庫已經不是 10 年前的數據庫了,在處理高并發寫的能力上已經不是同日而語了。其實并發沖突并沒有你想的那么嚴重,一般通過 select before update ,以及 version 樂觀鎖定,就沒問題了。支付寶一天幾千萬比在線交易,全部是強一致性,不然不叫在線交易系統。聚合根的存儲屬于單點存儲,不能用最終一致性。最終一致性是弱一致性的一種特殊方式,但是最終一致性往往用于處理分布式系統中同一份數據在多個地方有備份,然后可能會出現多個地方數據不一致的問題,但是最終都會一致即同步完成。具體大家可以看看 CAP 定理。 所謂的不變性約束是指:假設有一個采購訂單 Order ,一個 Order 下有多個訂單項 OrderItem ,假設有一個約束是,該采購訂單的總額不能超過 100 元。那么訂單的總額不能超過 100 元就是一個不變性約束;那么 Order 和 OrderItem 聚合在一起就顯得很有意義。在這種情況下,有 Order 來維護這個規則,當整個訂單被保存時,比如采用覆蓋的方式保存到數據庫。再舉個例子,比如一個論壇中有帖子和回復,大家都知道一個帖子有多個回復,回復離開帖子沒有意義。所以大家很自然會認為帖子和回復應該在一個聚合內,帖子是聚合根。但是這樣其實很有問題,仔細想想會發現帖子和回復之間并沒有不變性約束規則,回復和帖子之間只有一個簡單的 1:N 的關系而已。如果每次在添加一個回復時,都把帖子先取出來,然后在帖子的回復列表中把新的回復添加進去,然后再保存整個帖子,那么不難想象,這樣做無疑是小題大做,并且每次為了更新一個回復或新增一個回復,就要把整個帖子取出來,這樣做無疑非常浪費內存,并且在多用戶并發回同一個帖子的情況下則會更糟糕。實際上仔細分析一下,帖子和回復都應該是聚合,并且分別都是聚合根,我們要確保的僅僅是回復的帖子不能被修改即可。添加一個回復實際上和帖子無關,帖子根本不關心已經有多少個回復了。這點和之前的訂單的例子不同,訂單需要準確維護其包含的所有訂單項以便能夠計算出總價是否超出 100 元。其實這么多問題還是不足以詳細說明什么樣的對象該被聚合在一起,這里只是作為拋磚引玉,引發大家思考如何設計聚合。 一個聚合需要具備哪些更多的特征呢? 1 )需要具備前面說的基本特征; 2 )聚合內的子對象要么是值對象,要么是只讀的實體,為什么需要只讀,因為聚合的子實體是可以被臨時傳遞到外部的,要是外面的對象調用子對象的某個方法修改了子對象的屬性,那么就意味著繞過聚合根修改了聚合內的東西,這樣就無法確保聚合內的不變性了; 3 )如果聚合根有集合類型的屬性,那么該集合也必須是只讀的,即不允許別人在外部添加或刪除集合的元素,否則也同樣無法確保聚合的不變性。總之,我們要避免任何可能從外部修改聚合的行為發生,所有修改聚合的行為必須通過聚合根來實現。所以,理論上我們推薦大家在聚合內盡量設計值對象,原因大家多想想吧!其實從邏輯哲學的角度去思考,值對象表示了不變性,值對象表示一個值,值可以用來描述事物,事物就是實體。要是實體是由其他實體來描述,而其它實體是可變的,那么如何確保被描述的實體是可控的?大家想想為什么 DDD 書中,為什么要在 OrderItem 中存放當時購買時的 Price 就知道了。要是直接引用 Product 對象,那么會導致 OrderItem 引用了一個可變的對象,就無法確保訂單的不變性約束。而唯有持久一個不變的值對象,才能維持其不變性。 Evans 關于聚合的兩條推薦準則: 1 )聚合不要設計的過大,過大的聚合很難確保不變性,從而很難確保數據的強一致性; 2 )聚合與聚合之間不要通過引用的方式來關聯,而應該通過 ID 關聯,通過 ID 關聯也同樣能表示聚合之間的關系,并且具有更好的性能和可伸縮性,聚合根之間通過 ID 關聯的好處是:不會因為 Load 一個聚合根而把其他關聯的聚合根一起 Load 出來,這樣也避免了 Load 一個聚合根會把整個數據庫 Load 出來的風險;另外,對 ORM 的要求也很低,不需要 ORM 支持 LazyLoad ;聚合根與聚合根之間的關系不像聚合內的 Entity 之間這么強烈內聚,它們之間僅僅是某種比較弱的關聯關系,每個聚合根都有其獨立的生命周期; 聚合內的非跟的 Entity 以及 Value Object 之間不要相互引用,聚合內的所有 Child 可以對根 Entity 持有引用,如果一個 Child Entity 需要和另外一個 Child Entity 交互,則因該通過聚合根完成; 我們應該盡量減少聚合之間關聯,盡量做到單向關聯,只保留確實需要處理的經常需要用到的遍歷方向的關聯; 倉儲應理解為一個在內存中維護一系列聚合根的集合; 一個聚合根配備一個倉儲; 倉儲提供的接口應該總是接受聚合根或返回聚合根,不能返回聚合內的其他 Entity 或 Value Object ; 不要把倉儲理解為 DAO ,倉儲屬于領域模型的一部分,代表了領域模型向外提供接口的一部分,而 DAO 是表示數據庫向上層提供的接口表示; 倉儲的目的不是為了支持界面查詢,不要給倉儲中設計一些目的是為了為界面提供顯示數據的接口,倉儲提供的所有接口應該僅為領域模型使用;基本的倉儲接口只需要三個: Add , Remove , GetById ,其他的擴展接口可以根據業務需要擴展接口聲明; 如果一個操作僅由一個聚合根就可以完成,那么直接調用該聚合根完成即可; 領域服務表示領域模型中的一些業務操作,這些操作通常由多個聚合根或倉儲或其他領域服務相互協作完成,那么需要為這些操作建立領域服務,在領域服務中以過程化的方式來一步步首先根據各個聚合的 ID 獲取到操作的相關聚合根,然后調用聚合根完成整個業務操作;比如資金轉帳,這是經典的領域服務的例子;再比如在調用某個聚合根做一個數據更新之前需要先判斷一些業務規則,但是這些判斷規則不能在該聚合根內做,因為這樣做可能會導致聚合根依賴于外部的領域服務或倉儲,此時,應該交給領域服務來完成規則校驗和聚合根數據更新的整個過程。領域服務可以依賴倉儲或聚合根; 領域服務依賴倉儲時,工廠依賴于領域服務或倉儲時,都因該采用構造函數注入的方式,這樣可以避免領域模型中不會出現 DependencyResolver.Resolve<T>() 這樣的語句; 切忌不要因為領域服務的引入讓聚合根變得貧血,聚合根應該有的職責還是必須要由聚合根來承擔; 聚合根內不要依賴領域服務或倉儲,如果你發現一個聚合根的職責需要依賴于某個領域服務或倉儲來幫忙完成一些其他的邏輯(像判斷業務規則之類),那么通常你要考慮這個職責不應該由該聚合根來承擔,而應該建立合適的領域服務來承擔;聚合根的主要職責是管理其內聚的所有 Child Entity 或 Value Object 的業務完整性; 領域驅動設計時,為對象分配職責時,可以參考信息專家模式:將職責分配給擁有執行該職責所需信息的人;如果一個聚合根看起來擁有執行某個職責所需的信息,但沒包含全部所需信息,此時則不應該將該職責分配給該聚合根,因為強行分配給它,會導致該聚合根沒有內聚性,因為勢必會依賴于其它的領域對象或領域服務或倉儲; 要學習 CQRS 架構,要知道我們應該將應用程序的業務邏輯處理部分(即用戶命令響應部分)和查詢部分分離;我們應該用兩個不同的技術來實現這兩個部分的實現;用 DDD 領域模型來實現命令部分;用最快的查詢引擎來實現查詢部分; 如果要采用 CQRS 架構,我們需要考慮一個成熟可靠的底層框架,否則很容易導致命令端產生的領域對象的狀態無法同步(后者丟失)到查詢端的存儲中; 領域對象上的屬性可以具有 get 和 set ,因為我們平時所理解的對象不是真正的對象,而是某個事實的描述,比如圖書管理系統中的一個 Book 對象,表示圖書管中放著一本書,然后該書可能有一個入庫時間。現實生活正的話,書本的入庫時間絕對不可能變化,但是軟件中的 Book 因為不是真正的現實生活中的書本,而只是表示圖書館中有一本書這個事實的描述,我們當然可以修改這個事實,因為我們可能因為之前在書本入庫時所輸入的入庫時間是錯的,需要修改該入庫時間,此時就有提供 set 的必要了。所以,理論上任何一個 Entity ,除了 ID 之外,其他所有屬性都可以更改,因為這些屬性并不表示現實生活中的真正對象的特征,而僅僅只是對一個事實的描述;剛開始 Book 對象對書本入庫這個事實的描述可能有問題,此時我們就需要修改該 Book 的屬性;我想這個例子已經充分說明為什么可以提供 get 和 set 了; 不要總是零散的不加任何分組的設計 Entity 的屬性,因為有些屬性在邏輯上或業務上就是內聚的,代表一個完整的概念,比如 Country,Province,City,Town,Street ,等這些屬性表示一個地址的信息,此時我們應該設計一個 Address 對象來表示該地址信息,此時該 Address 就是一個值對象。所以我們在設計 Entity 的屬性時,要好好想想,哪些子屬性其實在業務上是一個完整的概念,此時我們就需要考慮將這些相關的屬性設計為一個值對象; 切忌值對象必須是只讀的,值對象之所以叫值對象最主要的是因為它表示一個值,而不是一個對象;值是不會變化的,是一個明確含義的不變的事物,比如 3 表示一個值,表述數量是 3 , 3 永遠不能變化;所以說,世界之所以存在,是因為有這些永恒不變的值對象的存在;我們只要把值對象理解為 3 ,“ abcd ”這樣的永恒不變的值就行了; 不要讓領域模型去模擬現實,模擬用戶(軟件使用者)與領域模型交互的過程;領域模型要實現的應該是用戶的需求,領域模型中不應該包含用戶的成分,想想只有空杯子才能裝水的道理,即無為以之用的道理就明白了;所以,我們在設計領域模型時首先要明白領域模型要完成的事情是什么;這方面,多看看用例圖,就知道軟件該做的事情了,推薦大家看的書是: Craig Larman 寫的《 UML 和模式應用》一書,非常經典; DDD中強調“領域對象是擁有行為的”。這句話我覺得說法是正確的,但是其做法難道就是“在領域對象里寫方法”這么簡單嗎? 我們常說“類應該具有生命的”,但我不認為“把方法寫到類里就會讓類具有生命了”,因為"把簡單地把方法寫到類里,其最終也只是讓類變成了一潭死水,是經不起風浪的,是無法變成湖泊和海洋的”。類不會無緣無故產生行為,類能夠產生行為一定是在一定的場景下發生的。在我看來,“簡單地把方法寫進類里是無法描述多(復雜)場景下的類的行為的”。如果有人說在他的項目里那樣做沒問題,那我只能說他的項目(場景)還不夠復雜。 其實“貧血對象”和“充血對象”都是極端的做法,而問題的關鍵是“類如何合理而自然地擁有行為”。在我看來,我們只能在“貧血對象”和“充血對象”之間達成一種平衡。如何把“場景”更好地融入 DDD 還有沒公論,但我想“類合理而自然地擁有行為”應該是一條準則。
與50位技術專家面對面 20年技術見證,附贈技術全景圖
總結
以上是生活随笔 為你收集整理的DDD关键知识点整理汇总 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。