聚合(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
public interface IAggregateRoot
{
}
聚合根
[AggregateRoot("Orders")]
partial class Order : IAggregateRoot
{
public Single TotalDiscount
{
get
{
return this.Lines.Sum(p => p.Discount);
}
}
public Single TotalAmount
{
get
{
return this.Lines.Sum(p => p.LineAmount);
}
}
}
到這裡又有問題了,接口IAggregateRoot中什麼都沒有定義?!我在我的技術博客中,特別解釋了C#中接口的三種用途,請參考這篇文章:《C#基礎:多功能的接口》。在這裡,我們將IAggregateRoot接口用作泛型約束。在看完後續的兩篇介紹領域對象生命周期的文章後,你就能夠更好地理解這個問題了。事實上,在領域驅動設計的社區中,不少人都是這樣用的。
最後說明一下,由於實體框架使所有的實體類繼承於EntityObject類,而從面向對象的角度,接口是沒辦法去繼承於類的,因此,在這裡我們的 IAggregateRoot接口好像跟實體沒什麼太大的關系,而事實上聚合根應該是一種實體。在很多領域驅動的項目中,設計人員專門設計了 IEntity接口,所有實現了該接口的類都被認定為實體類,於是,IAggregateRoot接口也就很自然地繼承IEntity接口,以表示“聚合根是一種實體”的概念,代碼大致如下:
IAggregateRoot
public interface IEntity
{
Guid Id { get; set; }
}
public interface IAggregateRoot : IEntity
{
}
總的來說,領域模型需要根據領域概念分成多個聚合,每個聚合都有一個實體作為“聚合根”,通俗地說,領域對象從無到有的創建,以及CRUD操作都應該作用在聚合根上,而不是單獨的某個實體。當你的代碼需要直接對聚合內部的實體進行CRUD操作時,就說明你的模型設計已經存在問題了。