聚合
聚合(Aggregate)是領域驅動設計中非常重要的一個概念。簡單地說,聚合是這樣一組領域對象(包括實體和值對象),這組領域對象聯合起來表述一個完整的領域概念。比如,根據Eric Evans《領域驅動設計》一書中的例子,一輛車包含四個輪子,輪子離開“車”就毫無意義,此時這個聯合體就是聚合,而“車”就是聚合根(Aggregate Root)。
從實踐中得知,並非領域模型中的每個實體都能夠完整地表述一個明確的領域概念,就比如客戶與送貨地址的關係。假設在某個應用中,系統需要爲每個客戶維護多個送貨地址,此時送貨地址就是一個實體,而不是值對象。那麼這樣一來,領域模型中至少就有了“客戶”和“送貨地址”兩個實體,而事實上,“送貨地址”是針對“客戶”的,離開“客戶”,“送貨地址”就變得毫無意義。於是,“送貨地址”就和“客戶”一起,完整地表達了“客戶可以有多個送貨地址,並能對它們進行維護”的思想。
在《實體框架之領域驅動實踐(三) - 案例:一個簡易的銷售系統》一文中,我們簡單地設計了一個領域模型,其中包含了一些必要的實體和值對象。現在,我用不同顏色的筆在這個領域模型上圈出了三個聚合:客戶、訂單以及產品分類,如下圖所示:
【注意】:如果像上圖所示,Category-Item組成一個聚合,那麼此時聚合根就應該是Item,而不是Category,因爲Category對Item從概念上並沒有包含/被包含的關係,而更多情況下,Category是 Item的一種信息描述,即某個Item是可以歸類到某個Category的。在這種情況下,我們不需要對Category進行維護,Category就以值對象的形式存在於領域模型中。如果是另一種應用場合,比如,我們的系統需要針對Category進行促銷,那麼我們需要維護Category的信息,由此Category和Item就分屬兩個不同的聚合,聚合根爲各自本身。
首先是“客戶-信用卡”聚合,這個聚合表示了一個客戶可以擁有多張信用卡,類似於上面所講的 “客戶-送貨地址”的概念;其次是“訂單-訂單行”的聚合,類似地,雖然訂單行也是一個實體,因爲在應用中需要對每個訂單行進行區分,但是訂單行離開訂單就變得毫無意義,它是“訂單”概念的一部分;最後是“產品分類-產品”的聚合。
每個聚合都有一個根實體(聚合根,Aggregate Root),這個根實體是聚合所表述的領域概念的主體,外部對象需要訪問聚合內的實體時,只能通過聚合根進行訪問,而不能直接訪問。從技術角度考慮,聚合確定了實體生命週期的關注範圍,即當某個實體被創建時,同時需要創建以其爲根的整個聚合,而當持久化某個實體時,同樣也需要持久化整個聚合。比如,在從外部持久化機制重建“客戶”對象的同時,也需要將其所擁有的“信用卡”賦給“客戶”實體(具體如何操作,根據需求而定)。不要去關注聚合內實體的生命週期問題,如果你真的這麼做了,那麼你就需要考慮下你的設計是否合理。
由此引出了“領域對象生命週期”的問題,這個問題我會在後面兩節單獨討論,但目前至少知道:
- 領域對象從無到有的創建,不是針對某個實體的,而是針對某個聚合的
- 領域對象的持久化(通常所說的“保存”)、重建(通常所說的“查詢”)和銷燬(通常所說的“刪除”)也不是針對某個實體的,而是針對某個聚合的
很可惜,微軟的EntityFramework(實體框架,EF)目前並不支持“聚合”的概念,所有的實體都被一股腦地塞到 ObjectContext中:
爲了實現聚合的概念,我們又一次地需要用到“部分類(partial class)”的功能。我們首先定義一個IAggregateRoot的接口,修改每個聚合根的實體類,使其實現IAggregateRoot接口,如下:
到這裏又有問題了,接口IAggregateRoot中什麼都沒有定義?!我在我的技術博客中,特別解釋了C#中接口的三種用途,請參考這篇文章:《C#基礎:多功能的接口》。在這裏,我們將IAggregateRoot接口用作泛型約束。在看完後續的兩篇介紹領域對象生命週期的文章後,你就能夠更好地理解這個問題了。事實上,在領域驅動設計的社區中,不少人都是這樣用的。
最後說明一下,由於實體框架使所有的實體類繼承於EntityObject類,而從面向對象的角度,接口是沒辦法去繼承於類的,因此,在這裏我們的 IAggregateRoot接口好像跟實體沒什麼太大的關係,而事實上聚合根應該是一種實體。在很多領域驅動的項目中,設計人員專門設計了 IEntity接口,所有實現了該接口的類都被認定爲實體類,於是,IAggregateRoot接口也就很自然地繼承IEntity接口,以表示“聚合根是一種實體”的概念,代碼大致如下:
總的來說,領域模型需要根據領域概念分成多個聚合,每個聚合都有一個實體作爲“聚合根”,通俗地說,領域對象從無到有的創建,以及CRUD操作都應該作用在聚合根上,而不是單獨的某個實體。當你的代碼需要直接對聚合內部的實體進行CRUD操作時,就說明你的模型設計已經存在問題了。
-----【以下爲原文網友評論及回覆信息】----- |
[ 2010-1-11 9:22:00 | By: ruson(遊客) ]
理論上是很好的,但實踐中感覺有些侷限性。
以下爲blog主人的回覆: |
[ 2010-1-16 9:17:00 | By: ruson(遊客) ]
你好,按現實中的常規,應該先有分類纔有分類下的子項,Item是屬於Category下的一個集合,Category優先於Item而存在。
以下爲blog主人的回覆: |
[ 2010-1-18 15:26:00 | By: ruson(遊客) ]
謝謝博主的回覆。
以下爲 blog主人的回覆: |