分享我的:領域驅動設計(DDD)學習成果精簡總結

tangxuehua發表於2011-12-03
1. 建立領域物件採用建構函式或者工廠,如果用工廠時需要依賴於領域服務或倉儲,則透過建構函式注入到工廠;
2. 一個聚合是由一些列相聯的Entity和Value Object組成,一個聚合有一個聚合根,聚合根是Entity,整個聚合被看成是一個資料修改的單元,也就是說整個聚合內的所有物件要麼同時被儲存,要麼都不能儲存,即儲存到資料持久層時必須以覆蓋的方式來儲存,而不是追加方式或合併的方式來儲存,否則無法確保聚合內的物件的資料一致性。作為推導的一個結論:我們不能只儲存一個聚合內的一部分物件;聚合內的所有實體和值物件應該總是一起被取出來一起被儲存,因為一個聚合是一個資料持久化的單元,不需要考慮將整個聚合根取出來有效能問題,因為任何一個聚合根都有明確的邊界。目前的記憶體快取框架都已發展的比較成熟,效能已經不是問題;如MongoDb,MemCache,NoSQL,等等;
3. 聚合內的物件之所以聚合在一起的關鍵原因不是因為它們具有一些關聯關係或依賴關係,而是因為聚合內的物件之間具有某些不變性規則,在任何時候,聚合內的所有這些物件必須滿足這些不變性規則。所以,如果一些物件之間看似有一些關聯關係或依賴關係,但是他們之間不具有任何不變性約束,那麼就不應該把這些物件放在一個聚合中,否則只是增加物件之間不必要的耦合性,增加物件維護的難度;
4. 所謂的不變性約束是指:假設有一個採購訂單Order,一個Order下有多個訂單項OrderItem,假設有一個約束是,該採購訂單的總額不能超過100元。那麼訂單的總額不能超過100元就是一個不變性約束;那麼Order和OrderItem聚合在一起就顯得很有意義。在這種情況下,有Order來維護這個規則,當整個訂單被儲存時,比如採用覆蓋的方式儲存到資料庫。再舉個例子,比如一個論壇中有帖子和回覆,大家都知道一個帖子有多個回覆,回覆離開帖子沒有意義。所以大家很自然會認為帖子和回覆應該在一個聚合內,帖子是聚合根。但是這樣其實很有問題,因為你仔細想想會發現帖子和回覆之間沒有不變性約束規則,回覆和帖子之間只有一個簡單的1:N的關係而已。如果每次在新增一個回覆時,都把帖子先取出來,然後再帖子的回覆列表中把新的回覆新增進去,然後再儲存整個帖子,那麼不難想想,在多使用者併發回同一個帖子的時候一定會造成帖子不能儲存的情況,因為在儲存帖子時是採用覆蓋的方式,但是發現別人已經回帖了。所以不能覆蓋,如果繼續覆蓋不管的話,那就會導致在你之前別人的回覆會被沖掉;實際上仔細分析一下,帖子和回覆都應該是聚合,並且分別都是聚合根,我們要確保的僅僅是回覆的帖子不能被修改即可。新增一個回覆實際上和帖子無關,帖子根本不關心已經有多少個回覆了。這點和之前的訂單的例子不同,訂單需要準確維護其包含的所有訂單項以便能夠計算出總價是否超出100元。其實這麼多問題還是不足以詳細說明什麼樣的物件該被聚合在一起,這裡只是作為拋磚引玉,引發大家思考如何設計聚合。
5. Evans關於聚合的兩條推薦準則:1)聚合不要設計的過大,過大的聚合很難確保不變性,從而很難確保資料的強一致性;2)聚合與聚合之間不要透過引用的方式來關聯,而應該透過ID關聯,透過ID關聯也同樣能表示聚合之間的關係,並且具有更好的效能和可伸縮性,聚合根之間透過ID關聯的好處是:不會因為Load一個聚合根而把其他關聯的聚合根一起Load出來,這樣也避免了Load一個聚合根會把整個資料庫Load出來的風險;另外,對ORM的要求也很低,不需要ORM支援LazyLoad;聚合根與聚合根之間的關係不像聚合內的Entity之間這麼強烈內聚,它們之間僅僅是某種比較弱的關聯關係,每個聚合根都有其獨立的生命週期;
6. 聚合內的非跟的Entity以及Value Object之間不要相互引用,聚合內的所有Child可以對根Entity持有引用,如果一個Child Entity需要和另外一個Child Entity互動,則因該透過聚合根完成;
7. 我們應該儘量減少聚合之間關聯,儘量做到單向關聯,只保留確實需要處理的經常需要用到的遍歷方向的關聯;
8. 倉儲應理解為一個在記憶體中維護一系列聚合根的集合;
9. 一個聚合根配備一個倉儲;
10.倉儲提供的介面應該總是接受聚合根或返回聚合根,不能返回聚合內的其他Entity或Value Object;
11.不要把倉儲理解為DAO,倉儲屬於領域模型的一部分,代表了領域模型向外提供介面的一部分,而DAO是表示資料庫向上層提供的介面表示;
12.倉儲的目的不是為了支援介面查詢,不要給倉儲中設計一些目的是為了為介面提供顯示資料的介面,倉儲提供的所有介面應該僅為領域模型使用;基本的倉儲介面只需要三個:Add,Remove,GetById,其他的擴充套件介面可以根據業務需要擴充套件介面宣告;
13.如果一個操作僅由一個聚合根就可以完成,那麼直接呼叫該聚合根完成即可;
14.領域服務表示領域模型中的一些業務操作,這些操作通常由多個聚合根或倉儲或其他領域服務相互協作完成,那麼需要為這些操作建立領域服務,在領域服務中以過程化的方式來一步步首先根據各個聚合的ID獲取到操作的相關聚合根,然後呼叫聚合根完成整個業務操作;比如資金轉帳,這是經典的領域服務的例子;再比如在呼叫某個聚合根做一個資料更新之前需要先判斷一些業務規則,但是這些判斷規則不能在該聚合根內做,因為這樣做可能會導致聚合根依賴於外部的領域服務或倉儲,此時,應該交給領域服務來完成規則校驗和聚合根資料更新的整個過程。領域服務可以依賴倉儲或聚合根;
15.領域服務依賴倉儲時,工廠依賴於領域服務或倉儲時,都因該採用建構函式注入的方式,這樣可以避免領域模型中不會出現DependencyResolver.Resolve<T>()這樣的語句;
16.切忌不要因為領域服務的引入讓聚合根變得貧血,聚合根應該有的職責還是必須要由聚合根來承擔;
17.聚合根內不要依賴領域服務或倉儲,如果你發現一個聚合根的職責需要依賴於某個領域服務或倉儲來幫忙完成一些其他的邏輯(像判斷業務規則之類),那麼通常你要考慮這個職責不應該由該聚合根來承擔,而應該建立合適的領域服務來承擔;聚合根的主要職責是管理其內聚的所有Child Entity或Value Object的業務完整性;
18.領域驅動設計時,為物件分配職責時,可以參考資訊專家模式:將職責分配給擁有執行該職責所需資訊的人;如果一個聚合根看起來擁有執行某個職責所需的資訊,但沒包含全部所需資訊,此時則不應該將該職責分配給該聚合根,因為強行分配給它,會導致該聚合根沒有內聚性,因為勢必會依賴於其它的領域物件或領域服務或倉儲;
19.要學習CQRS架構,要知道我們應該將應用程式的業務邏輯處理部分(即使用者命令響應部分)和查詢部分分離;我們應該用兩個不同的技術來實現這兩個部分的實現;用DDD領域模型來實現命令部分;用最快的查詢引擎來實現查詢部分;
20.如果要採用CQRS架構,我們需要考慮一個成熟可靠的底層框架,否則很容易導致命令端產生的領域物件的狀態無法同步(後者丟失)到查詢端的儲存中;
21.領域物件上的屬性可以具有get和set,因為我們平時所理解的物件不是真正的物件,而是某個事實的描述,比如圖書管理系統中的一個Book物件,表示圖書管中放著一本書,然後該書可能有一個入庫時間。現實生活正的話,書本的入庫時間絕對不可能變化,但是軟體中的Book因為不是真正的現實生活中的書本,而只是表示圖書館中有一本書這個事實的描述,我們當然可以修改這個事實,因為我們可能因為之前在書本入庫時所輸入的入庫時間是錯的,需要修改該入庫時間,此時就有提供set的必要了。所以,理論上任何一個Entity,除了ID之外,其他所有屬性都可以更改,因為這些屬性並不表示現實生活中的真正物件的特徵,而僅僅只是對一個事實的描述;剛開始Book物件對書本入庫這個事實的描述可能有問題,此時我們就需要修改該Book的屬性;我想這個例子已經充分說明為什麼可以提供get和set了;
22.不要總是零散的不加任何分組的設計Entity的屬性,因為有些屬性在邏輯上或業務上就是內聚的,代表一個完整的概念,比如Country,Province,City,Town,Street,等這些屬性表示一個地址的資訊,此時我們應該設計一個Address物件來表示該地址資訊,此時該Address就是一個值物件。所以我們在設計Entity的屬性時,要好好想想,哪些子屬性其實在業務上是一個完整的概念,此時我們就需要考慮將這些相關的屬性設計為一個值物件;
23.切忌值物件必須是隻讀的,值物件之所以叫值物件最主要的是因為它表示一個值,而不是一個物件;值是不會變化的,是一個明確含義的不變的事物,比如3表示一個值,表述數量是3,3永遠不能變化;所以說,世界之所以存在,是因為有這些永恆不變的值物件的存在;我們只要把值物件理解為3,“abcd”這樣的永恆不變的值就行了;
24.不要讓領域模型去模擬現實,模擬使用者(軟體使用者)與領域模型互動的過程;領域模型要實現的應該是使用者的需求,領域模型中不應該包含使用者的成分,想想只有空杯子才能裝水的道理,即無為以之用的道理就明白了;所以,我們在設計領域模型時首先要明白領域模型要完成的事情是什麼;這方面,多看看用例圖,就知道軟體該做的事情了,推薦大家看的書是:Craig Larman寫的《UML和模式應用》一書,非常經典;
25.是該學習一下什麼是強一致性,什麼是最終一致性的時候了,必須瞭解什麼是CAP定理

相關文章