EntityFramework之領域驅動設計實踐(五)

聚合

聚合(Aggregate)是領域驅動設計中非常重要的一個概念。簡單地說,聚合是這樣一組領域對象(包括實體和值對象),這組領域對象聯合起來表述一個完整的領域概念。比如,根據Eric Evans《領域驅動設計》一書中的例子,一輛車包含四個輪子,輪子離開“車”就毫無意義,此時這個聯合體就是聚合,而“車”就是聚合根(Aggregate Root)。

從實踐中得知,並非領域模型中的每個實體都能夠完整地表述一個明確的領域概念,就比如客戶與送貨地址的關係。假設在某個應用中,系統需要爲每個客戶維護多個送貨地址,此時送貨地址就是一個實體,而不是值對象。那麼這樣一來,領域模型中至少就有了“客戶”和“送貨地址”兩個實體,而事實上,“送貨地址”是針對“客戶”的,離開“客戶”,“送貨地址”就變得毫無意義。於是,“送貨地址”就和“客戶”一起,完整地表達了“客戶可以有多個送貨地址,並能對它們進行維護”的思想。

《實體框架之領域驅動實踐(三) - 案例:一個簡易的銷售系統》一文中,我們簡單地設計了一個領域模型,其中包含了一些必要的實體和值對象。現在,我用不同顏色的筆在這個領域模型上圈出了三個聚合:客戶、訂單以及產品分類,如下圖所示:

 

24150813078

 

 

【注意】:如果像上圖所示,Category-Item組成一個聚合,那麼此時聚合根就應該是Item,而不是Category,因爲Category對Item從概念上並沒有包含/被包含的關係,而更多情況下,Category是 Item的一種信息描述,即某個Item是可以歸類到某個Category的。在這種情況下,我們不需要對Category進行維護,Category就以值對象的形式存在於領域模型中。如果是另一種應用場合,比如,我們的系統需要針對Category進行促銷,那麼我們需要維護Category的信息,由此Category和Item就分屬兩個不同的聚合,聚合根爲各自本身。

首先是“客戶-信用卡”聚合,這個聚合表示了一個客戶可以擁有多張信用卡,類似於上面所講的 “客戶-送貨地址”的概念;其次是“訂單-訂單行”的聚合,類似地,雖然訂單行也是一個實體,因爲在應用中需要對每個訂單行進行區分,但是訂單行離開訂單就變得毫無意義,它是“訂單”概念的一部分;最後是“產品分類-產品”的聚合。

每個聚合都有一個根實體(聚合根,Aggregate Root),這個根實體是聚合所表述的領域概念的主體,外部對象需要訪問聚合內的實體時,只能通過聚合根進行訪問,而不能直接訪問。從技術角度考慮,聚合確定了實體生命週期的關注範圍,即當某個實體被創建時,同時需要創建以其爲根的整個聚合,而當持久化某個實體時,同樣也需要持久化整個聚合。比如,在從外部持久化機制重建“客戶”對象的同時,也需要將其所擁有的“信用卡”賦給“客戶”實體(具體如何操作,根據需求而定)。不要去關注聚合內實體的生命週期問題,如果你真的這麼做了,那麼你就需要考慮下你的設計是否合理。

由此引出了“領域對象生命週期”的問題,這個問題我會在後面兩節單獨討論,但目前至少知道:

  1. 領域對象從無到有的創建,不是針對某個實體的,而是針對某個聚合的
  2. 領域對象的持久化(通常所說的“保存”)、重建(通常所說的“查詢”)和銷燬(通常所說的“刪除”)也不是針對某個實體的,而是針對某個聚合的

很可惜,微軟的EntityFramework(實體框架,EF)目前並不支持“聚合”的概念,所有的實體都被一股腦地塞到 ObjectContext中:

241532814506

爲了實現聚合的概念,我們又一次地需要用到“部分類(partial class)”的功能。我們首先定義一個IAggregateRoot的接口,修改每個聚合根的實體類,使其實現IAggregateRoot接口,如下:

隱藏行號 複製代碼 IAggregateRoot
  1. public interface IAggregateRoot
    
  2. {
    
  3. }
    
  4. 
    
隱藏行號 複製代碼 聚合根
  1. [AggregateRoot("Orders")]
    
  2. partial class Order : IAggregateRoot
    
  3. {
    
  4.     public Single TotalDiscount
    
  5.     {
    
  6.         get
    
  7.         {
    
  8.             return this.Lines.Sum(p => p.Discount);
    
  9.         }
    
  10.     }
    
  11. 
    
  12.     public Single TotalAmount
    
  13.     {
    
  14.         get
    
  15.         {
    
  16.             return this.Lines.Sum(p => p.LineAmount);
    
  17.         }
    
  18.     }
    
  19. 
    
  20. }
    
  21. 
    

到這裏又有問題了,接口IAggregateRoot中什麼都沒有定義?!我在我的技術博客中,特別解釋了C#中接口的三種用途,請參考這篇文章:《C#基礎:多功能的接口》。在這裏,我們將IAggregateRoot接口用作泛型約束。在看完後續的兩篇介紹領域對象生命週期的文章後,你就能夠更好地理解這個問題了。事實上,在領域驅動設計的社區中,不少人都是這樣用的。

最後說明一下,由於實體框架使所有的實體類繼承於EntityObject類,而從面向對象的角度,接口是沒辦法去繼承於類的,因此,在這裏我們的 IAggregateRoot接口好像跟實體沒什麼太大的關係,而事實上聚合根應該是一種實體。在很多領域驅動的項目中,設計人員專門設計了 IEntity接口,所有實現了該接口的類都被認定爲實體類,於是,IAggregateRoot接口也就很自然地繼承IEntity接口,以表示“聚合根是一種實體”的概念,代碼大致如下:

隱藏行號 複製代碼 IAggregateRoot
  1. public interface IEntity
    
  2. {
    
  3.     Guid Id { get; set; }
    
  4. }
    
  5. public interface IAggregateRoot : IEntity
    
  6. {
    
  7.     
    
  8. }
    
  9. 
    

總的來說,領域模型需要根據領域概念分成多個聚合,每個聚合都有一個實體作爲“聚合根”,通俗地說,領域對象從無到有的創建,以及CRUD操作都應該作用在聚合根上,而不是單獨的某個實體。當你的代碼需要直接對聚合內部的實體進行CRUD操作時,就說明你的模型設計已經存在問題了。

 

 

-----【以下爲原文網友評論及回覆信息】-----
 

Re:實體框架之領域驅動實踐(五)

[ 2010-1-11 9:22:00 | By: ruson(遊客) ]

理論上是很好的,但實踐中感覺有些侷限性。
如上文所說Category和Item是一個聚合,Category是聚合根。那如果一個apsx頁中要打開id爲1的Item信息,是否還要把 Category的id也傳過來。先從數據庫中取出Category,再從Category中取Item呢。

以下爲blog主人的回覆:
你的問題很有價值!
Category可以是聚合根,也可以不是,應該根據實際情況進行考慮。如果我們不需要對Category進行維護,那麼將Category和Item劃歸一個聚合,聚合根應該是Item而不是Category,也就是說,Category應該是Item的一部分,Category作爲值對象存在。考慮另外一種應用場合,比如在訂單上使用銷售折扣,這個折扣可以應用在Item上,也允許應用在Category上(比如某個客戶如果買了這個Category 下的Item,那麼就按多少的折扣給他),那麼此時就不得不去維護Category的信息,於是,Item和Category分屬兩個不同的聚合,聚合根爲其本身。
這裏也讓我們瞭解到,實體和值對象沒有明確的分界線,只能是設計人員在實踐中根據自己的實際經驗把握。
【另外,對於文章中給大家造成的不合理引導,我深表歉意,我會在文章中保留原本錯誤的部分,然後將勘誤用橙色筆標註以備參考。也非常歡迎大家能夠提出自己的問題與疑惑】

Re:實體框架之領域驅動實踐(五)

[ 2010-1-16 9:17:00 | By: ruson(遊客) ]

你好,按現實中的常規,應該先有分類纔有分類下的子項,Item是屬於Category下的一個集合,Category優先於Item而存在。
在Item必需有分類的情況下,Item離開了Category就顯得無意義,所以我的想法是Category應該爲聚合根。但會遇到上面我所提到的問題。以及如果只想對其中某一個Item進行CURD時還要先取出來整個聚合根的話對性能的影響有些疑問。
謝謝。

以下爲blog主人的回覆:
你好!這個問題確實讓人難以理解,開始的時候,我承認我自己也沒有經過深思熟慮就把結論寫在這裏,造成很多朋友的誤解,再次深表歉意。
由於你現在需要對Item進行CRUD,更“領域”一點講,你需要對Item進行持久化的操作,那麼Item就一定是聚合根,那麼它是哪個聚合的聚合根?就需要看應用本身的需求了。
同理,至於Category和Item是否屬於同一個聚合,以及Category是否是聚合根,也要根據實際需求而定。DDD中所討論的聚合應該是組合聚合(Composite Aggregation),而不是可共享聚合(Shareable Aggregation),因爲在創建聚合的同時也需要創建聚合內部的成員。於是,從語義上講,子部件無法脫離聚合而單獨存在(就像汽車與方向盤、輪子的關係那樣)。但事實上呢?現實生活中,物品就是物品,可以不給它們歸類,也可以將它們分屬於不同的類別,但歸類也好,不歸類也好,物品都是客觀存在的。不因爲你不給它歸類,物品就消失了。
因此,在做建模的時候,我們可能需要更加註重實際應用與我們模型的距離,以便更加真實客觀地反映問題本身。DDD是實踐指導,不是理論,就像對待設計模式一樣,我們所能做的只是借鑑,而不是照搬。
希望我的解答能夠爲你提供幫助。

Re:實體框架之領域驅動實踐(五)

[ 2010-1-18 15:26:00 | By: ruson(遊客) ]

謝謝博主的回覆。
這段時間在嘗試NHibernate + Spring.NET下的DDD實踐。

以下爲 blog主人的回覆:
共同探討,共同進步。
2008年的時候我嘗試過Spring.NET+Castle ActiveRecord的DDD實踐。當時我選用Castle ActiveRecord的原因是因爲樣品很簡單,我不打算去維護NHibernate複雜的mapping XML。其實ActiveRecord是夾在Transaction Script和Domain Model之間的DDD的反模式,DDD社區中不少人指出這種做法不妥,但我覺得只要適合我的實際情況,也沒什麼大礙。
與Spring.NET一樣,NHibernate也是一種解耦的手段。Spring.NET解耦了對象之間的依賴性,而NHibernate則解耦了對象模型與數據庫模型之間的映射關係。兩者目的相同:提高系統的延展性和擴充性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章