聚合作為領域模型中重要的業務功能單元,它的設計是領域建模過程中非常重要的工作。其中聚合根的判斷並非一件易事,往往給人一種似是而非的感覺,讓人難以捉摸,陷入兩難的境地。今天筆者就想以部落格園為例來探討下:部落格 (Blog) 和評論 (Comment) 究竟是不是一個聚合?
眾所周知,在部落格這個領域中,核心子域就是寫部落格。從部落格這個限界上下文中,我們很容易提煉出部落格和評論兩個領域物件,兩者之間是一種從屬關係。那麼我們該如何來進行聚合設計呢?先來回顧下DDD中聚合的概念:
聚合(Aggregate)定義了一組具有內聚關係的相關物件的集合,我們把聚合看作是一個修改資料的單元。每個聚合都含有一個根實體,叫做聚合根(Aggregate Root),它是聚合的管理者。
我想很多人心中已有答案:部落格和評論不就是一個具有從屬關係的聚合嗎?所有評論都是圍繞部落格而存在的,一旦部落格被刪除後,那麼評論也將不復存在。當我們要檢視或發表評論,我們必須先找到自己感興趣的部落格才行。試想,現實中存不存在這樣的業務場景,可以繞開部落格直接檢視或發表評論? 答案是否。因此在部落格這個聚合裡,部落格就是聚合根,而評論就是實體。如果僅靠從屬關係來判定聚合的話,筆者認為依據是不充分的,繼續往下看。
現在我們再來思考:部落格和評論除了從屬關係之外,兩者之間還存在哪些約束關係?根據部落格園現有的功能,我們可以得出以下業務規則:每個使用者都可以發表評論,但只能修改和刪除自己的評論,只有博主可以刪除別人評論。最終轉換成的業務程式碼,大致如下:
/// <summary> /// 部落格 /// </summary> public class Blog : IAggregateRoot { /// <summary> /// 部落格Id /// </summary> public Guid Id { get; private set; } /// <summary> /// 博主Id /// </summary> public Guid OwnerId { get; private set; } /// <summary> /// 評論集合 /// </summary> public ICollection<Comment> Comments { get; private set; } /// <summary> /// 新增評論 /// </summary> /// <param name="commentId">評論Id</param> /// <param name="commentId">評論內容</param> /// <param name="userId">使用者Id</param> public void AddComment(Guid commentId, string content, Guid userId) { var comment = new Comment(commentId, content, Id, userId); Comments.Add(comment); } /// <summary> /// 刪除評論 /// </summary> /// <param name="commentId">評論Id</param> /// <param name="userId">使用者Id</param> public void RemoveComment(Guid commentId, Guid userId) { var comment = Comments.Single(c => c.Id == commentId); if (comment.OwnerId != userId && OwnerId != userId) { throw new UserFriendlyException("不能刪除別人的評論"); } Comments.Remove(comment); } /// <summary> /// 修改評論 /// </summary> /// <param name="commentId">評論Id</param> /// <param name="content">評論內容</param> /// <param name="userId">使用者Id</param> public void UpdateComment(Guid commentId, string content, Guid userId) { var comment = Comments.Single(c => c.Id == commentId); if (comment.OwnerId != userId) { throw new UserFriendlyException("不能修改別人的評論"); } comment.Content = content; } }
從上述程式碼中,細心的網友不難發現:部落格和評論之間維持的是一種很弱的業務關係,而且每次增加、刪除或修改評論,都必須先把評論全部檢索出來(因為聚合是一個完整的資料單元)。肯定會有人質疑:這樣做是否有必要?如果評論很多的話,對查詢效能沒有影響嗎?這是筆者曾經看到過的一篇博文《部落格園的大牛們,被你們害慘了,Entity Framework從來都不需要去寫Repository設計模式》,評論數多達291條,從效能角度而言,這的確是一件令人擔憂的事情。於是,我們就該反思領域建模哪裡出了問題?
我們再來看評論這個領域物件,它本身是一個實體,且含有特定的業務規則:即每個使用者只能對別人的評論發表支援/反對意見,這樣才能客觀公正反映評論的價值。因此在評論這個領域中,評論和評論意見就是一個獨立的聚合。評論是聚合根,評論意見是實體。一旦評論被刪除,所有的支援/反對意見將毫無意義。
看到這裡,我們終於明白了一個真相:相同的領域物件在不同的上下文中,它既可以是實體,也可以是聚合根。回頭再看部落格園這個例子,部落格本身是聚合根沒錯,它除了和評論關聯之外,實際上還關聯了合集和標籤等其它領域物件。 如果把評論當成部落格聚合裡的實體的話,你會發現部落格這個聚合過於龐大,管理的實體太多,內部邏輯關係也更加複雜,同時還存在不可忽視的效能問題。因此,我們有必要將評論提升成為聚合根,這樣既避免了效能問題,同時也讓部落格聚合根的職責變得簡單。這裡請思考一個問題:假設部落格園改變業務規則,部落格可以關閉評論,且評論數量不得超過10條,那麼評論可否作為部落格聚合中的實體呢?
DDD中的聚合是可以拆分的最小功能單元,它是用來封裝真正的業務不變性,而不是簡單地將物件組合在一起。如果把聚合比作組織,那聚合根就是這個組織的管理者,負責協調實體和值物件,一起完成共同的業務邏輯。同時聚合的設計並不是一成不變的,需要根據業務規則和實際情況來調整,哪怕一開始建模是正確的。還有一點就是聚合要儘量設計得小,這樣獨立性才高,才能適應業務的變化。以上就是筆者對聚合的一些思考,如有不當之處,請指正。