從壹開始微服務 [ DDD ] 之六 ║聚合 與 聚合根 (下)


前言

哈嘍大家周二好,上次咱們說到了實體與值對象的簡單知識,相信大家也是稍微有些了解,其實實體咱們平時用的很多了,基本可以和數據庫表進行聯系,只不過值對象可能不是很熟悉,值對象簡單來說就是在DDD領域驅動設計中,為了更好的展示領域模型之間的關系,制定的一個對象,它沒有狀態和標識,目的就是為了表示一個值。今天呢本來不想說聚合了,因為網上的資料已經鋪天蓋地,想着開始說領域服務和領域事件了,但是為了本系列的完整性,今天就簡單的說一下聚合和聚合根的理解,,如果你已經很明白了,請指出我說的不足之處,以便可以讓大家知道,如果你還不是很明白,請看過后思考以下幾個問題,領域事件下次再說吧,這樣也就完成了今天的頭腦風暴:

1、什么是聚合?

2、聚合的作用是什么?

3、我們平時接觸到聚合了么?

 

這里有一個小Code,大家先看看這三者都是屬於什么(實體,值對象,聚合/聚合根):

  public class Order
    {
        public Guid Id;
        public string OrderNo;
        public Address Address;
        public List<OrderItem> Items;
        //...
    }

    public class OrderItem
    {
        public string Id;
        public float Price;
        public Goods Goods;
        public int Count;
        //...
    }

    public class Goods
    {
        public string Id;
        public string Name;
        //...
    }

    public class Address
    {
        public string Id;
        public string Country;
        public string Province;
        //...
    }

 

 

零、今天完成藍色區塊部分

 

 一、聚合的概念 —— 領域的核心

1、聚合的概念

 在DDD領域驅動設計第一次被提出的時候,聚合的概念就隨之而來了,在之前的文章中,我們說到了領域和子領域的划分,也說了限界上下文的定義,這些都是和我們平時以數據模型為中心所不同的概念,可能理解起來不是很容易,但是至少我們有了這個影子,想象着一個大的領域項目,根據業務來拆分成了多個子領域與上下文,可能不同的上下文中甚至有相似的概念,舉個栗子就是,訂單上下文有商品,物流上下文有貨物,庫存上下文有存貨等等等等,這時候你會發現,其實他們都是指的同一個東西,只不過在不同的上下文中被人為的賦予了不同的概念,有的是實體(庫存),有的是值對象(訂單),但是它們又不是一個概念,因為他們屬於不同的子領域。

 

這個時候,既然我們從大的方面已經對限界上下文進行分離整合,與之而來的肯定是領域模型的分離(我們肯定不能把每一個表放在一起,也不會把他們都一個個並列排開),那既然有分離肯定就是有聚合,這個時候,聚合就出來了,其實DDD提出聚合的概念是為了保證領域內對象之間的一致性問題,因為我們從上邊也看到了,在不同的地方會存在調用關系,當然主要還是子領域內部相互調用,

比如創建一個訂單,必然會生成訂單詳情,訂單詳情肯定會有商品信息,我們在修改商品信息的時候,肯定就不能影響到這個訂單詳情中的商品信息。再比如:用戶在下單的時候,會選擇一個地址作為郵寄地址,如果該用戶立刻下另一個訂單,並對自己個人中心的地址進行修改,肯定就不能影響剛剛下單的郵寄地址信息。

這個時候,聚合就有很強的作用,通過值對象保證了對象之間的一致性。我們平時在開發的時候,雖然沒有用到DDD,肯定也是經常用到聚合,就比如上邊的問題,撇開DDD不談,就平時來說,你肯定不會把商品 id 直接綁定到訂單詳情表中,為外鍵的,不然會死得很慘。這個時候其實我們就有一些聚合的概念了,因為什么呢,下單的時候,我們關注訂單領域模型,修改商品的時候,我們關注商品領域模型,這些就是我們說到的聚合,當然一個上下文會有很多聚合,而且聚合要盡可能的細分,那如何正確的區分聚合,以及以什么為基准,請往下看。

 

2、我們如何對聚合進行划分

1、哪些實體或值對象在一起才能夠有效的表達一個領域概念。

比如:訂單模型中,必須有訂單詳情,物流信息等實體或者值對象,這樣才能完整的表達一個訂單的領域概念,就比如文章開頭中提到的那個Code栗子中,OrderItem、Goods、Address等

2、確定好聚合以后,要確定聚合根

  比如:訂單模型中,訂單表就是整個聚合的聚合根。

 

 /// <summary>
    /// 聚合根 Order
    /// </summary>
    public class Order : AggregateRoot
    {
        public Guid Id;
        public string OrderNo;
        public Address Address;//值對象
        public List<OrderItem> Items;//實體集合
        //...
    }

 

3、對象之間是否必須保持一些固定的規則。

比如:Order(一 個訂單)必須有對應的客戶郵寄信息,否則就不能稱為一個有效的Order;同理,Order對OrderLineItem有不變性約束,Order也必須至少有一個OrderLineItem(一條訂單明細),否則就不能稱為一個有效的Order;

另外,Order中的任何OrderLineItem的數量都不能為0,否則認為該OrderLineItem是無效 的,同時可以推理出Order也可能是無效的。因為如果允許一個OrderLineItem的數量為0的話,就意味着可能會出現所有 OrderLineItem的數量都為0,這就導致整個Order的總價為0,這是沒有任何意義的,是不允許的,從而導致Order無效;所以,必須要求 Order中所有的OrderLineItem的數量都不能為0;那么現在可以確定的是Order必須包含一些OrderLineItem,那么應該是通 過引用的方式還是ID關聯的方式來表達這種包含關系呢?這就需要引出另外一個問題,那就是先要分析出是OrderLineItem是否是一個獨立的聚合 根。回答了這個問題,那么根據上面的規則就知道應該用對象引用還是用ID關聯了。

那么OrderLineItem是否是一個獨立的聚合根呢?因為聚合根意 味着是某個聚合的根,而聚合有代表着某個上下文邊界,而一個上下文邊界又代表着某個獨立的業務場景,這個業務場景操作的唯一對象總是該上下文邊界內的聚合 根。想到這里,我們就可以想想,有沒有什么場景是會繞開訂單直接對某個訂單明細進行操作的。也就是在這種情況下,我們 是以OrderLineItem為主體,完全是在面向OrderLineItem在做業務操作。有這種業務場景嗎?沒有,我們對 OrderLineItem的所有的操作都是以Order為出發點,我們總是會面向整個Order在做業務操作,比如向Order中增加明細,修改 Order的某個明細對應的商品的購買數量,從Order中移除某個明細,等等類似操作,我們從來不會從OrderlineItem為出發點去執行一些業 務操作;另外,從生命周期的角度去理解,那么OrderLineItem離開Order沒有任何存在的意義,也就是說OrderLineItem的生命周 期是從屬於Order的。所以,我們可以很確信的回答,OrderLineItem是一個實體。

 

4、聚合不要設計太大,否則會有性能問題以及業務規則一致性的問題。

 

對於大聚合,即便可以成功地保持事務一致性,但它可能限制了系統性能和可伸縮性。 系統可能隨著時間可能會有越來越多的需求與用戶,開發與維護的成本我們不應該忽視。

怎樣的聚合才算是"小"聚合呢??

好的做法是使用根實體(Root Entity)來表示聚合,其中只包含最小數量的屬性或值類型屬性。哪些屬性是所需的呢??簡單的答案是:那些必須與其他屬性保持一致的屬性。

比如,Product聚合內的name與description屬性,是需要保持一致的,把它們放在兩個不同的聚合顯然是不恰當的。

 

5、聚合中的實體和值對象應該具有相同的生命周期,並應該屬於一個業務場景。

 

比如一個最常見的問題:論壇發帖和回復如何將里聚合模型,大家想到這里,聯想到上邊的訂單和訂單詳情,肯定會peng peng的這樣定義;

    /// <summary>
    /// 聚合根 發帖
    /// </summary>
    public class Post : AggregateRoot
    {
        public string PostTitle;
        public List<Reply> Reply;//回復
        //...
    }
    /// <summary>
    /// 實體 回復
    /// </summary>
    public class Reply : Entity
    {
        public string Content;
        //...
    }

這樣初看是沒有什么問題,很正常呀,發帖子是發回復的聚合根,回復必須有一個帖子,不然無效,看似合理的地方卻有不合理。

比如,當我要對一個帖子發表回復時,我取出當前帖子信息,嗯,這個很對,但是,如果我對回復進行回復的時候,那就不好了,我每次還是都要取出整個帶有很多回復的帖子,然后往里面增加回復,然后保存整個帖子,因為聚合的一致性要求我們必須這么做。無論是在場景還是在並發的情況下這是不行的。

如果帖子和回復在一個聚合內,聚合意味着“修改數據的一個最小單元”,聚合內的所有對象要看成是一個整體最小單元進行保存。這么要求是因為聚合的意義是維護聚合內的不變性,數據一致性;
仔細分析我們會發現帖子和回復之間沒有數據一致性要求。所以不需要設計在同一個聚合內。

從場景的角度,我們有發表帖子,發表回復,這兩個不同的場景,發表帖子創建的是帖子,而發表回復創建的是回復。但是訂單就不一樣,我們有創建訂單,修改訂單這兩個場景。這兩個場景都是圍繞這訂單這個聚合展開的。

所以我們應該把回復實體也單獨作為一個聚合根來處理:

    /// <summary>
    /// 內容
    /// </summary>
    public class Content
    {
        public string Id;
        public DateTime DatePost;
        public string Contents;
        public string Title;
        //...
    }
    /// <summary>
    /// 聚合根 發帖
    /// </summary>
    public class Post : AggregateRoot,ContentBase
    {
        public string Title;
        //...
    }
    /// <summary>
    /// 聚合根 回復
    /// </summary>
    public class Reply : AggregateRoot,ContentBase
    {
        public Content Content;
        public Post Post;//帖子實體聚合根
        //...
    }

 當然這樣的話,我們就不能通過帖子一次性全部把回復拿出來,我們就只能單寫邏輯了,比如在應用層,但是這樣不會對領域層造成失血,因為本來就不是領域的問題。

 

二、聚合是如何聯系的

如何聯系,在上文的代碼中以及由體現了,這里用文字來說明下,具體的可以參考文中的代碼

1、聚合根、實體、值對象的區別?

從標識的角度:

聚合根具有全局的唯一標識,而實體只有在聚合內部有唯一的本地標識,值對象沒有唯一標識,不存在這個值對象或那個值對象的說法;

從是否只讀的角度:

聚合根除了唯一標識外,其他所有狀態信息都理論上可變;實體是可變的;值對象是只讀的;

從生命周期的角度:

聚合根有獨立的生命周期,實體的生命周期從屬於其所屬的聚合,實體完全由其所屬的聚合根負責管理維護;值對象無生命周期可言,因為只是一個值;

2、聚合根、實體、值對象對象之間如何建立關聯?

聚合根到聚合根:通過ID關聯;

聚合根到其內部的實體,直接對象引用;

聚合根到值對象,直接對象引用;

實體對其他對象的引用規則:1)能引用其所屬聚合內的聚合根、實體、值對象;2)能引用外部聚合根,但推薦以ID的方式關聯,另外也可以關聯某個外部聚合內的實體,但必須是ID關聯,否則就出現同一個實體的引用被兩個聚合根持有,這是不允許的,一個實體的引用只能被其所屬的聚合根持有;

值對象對其他對象的引用規則:只需確保值對象是只讀的即可,推薦值對象的所有屬性都盡量是值對象;

3、如何識別聚合與聚合根?

明確含義:一個Bounded Context(界定的上下文)可能包含多個聚合,每個聚合都有一個根實體,叫做聚合根;

識別順序:先找出哪些實體可能是聚合根,再逐個分析每個聚合根的邊界,即該聚合根應該聚合哪些實體或值對象;最后再划分Bounded Context;

聚合邊界確定法則:根據不變性約束規則(Invariant)。不變性規則有兩類:1)聚合邊界內必須具有哪些信息,如果沒有這些信息就不能稱為一個有效的聚合;2)聚合內的某些對象的狀態必須滿足某個業務規則;

1.一個聚合只有一個聚合根,聚合根是可以獨立存在的,聚合中其他實體或值對象依賴與聚合根。

2.只有聚合根才能被外部訪問到,聚合根維護聚合的內部一致性。

 

三、聚合的一些思考

1、優點

其實整篇文章都是在說的聚合的優點,這里簡單再概況下:

 聚合的出現,很大程度上,幫助了DDD領域驅動設計的全部普及,試想一下,如果沒有聚合和聚合根的思維,單單來說DDD,總感覺不是很舒服,而且領域驅動設計所分的子領域和限界上下文都是從更高的一個層面上來區分的,有的項目甚至只有一個限界上下文,那么,聚合的思考和使用,就特別的高效,且有必要。

 聚合設計的原則應該是聚合內各個有相互關聯的對象之間要保持 不變性!我們平時設計聚合時,一般只考慮到了對象之間的關系,比如看其是否能獨立存在,是否必須依賴與某個其他對象而存在。

 

2、擔憂

我接觸的DDD中的聚合根的分析設計思路大致是這樣:1、業務本質邏輯分析;2、確認聚合對象間的組成關系;3、所有的讀寫必須沿着這些固有的路徑進行。
這是一種靜態聚合的設計思路。理論上講,似乎沒有什么問題。但實際上,因為每一個人的思路以及學習能力,甚至是專業領域知識的不同,會導致設計的不合理,特別是按照這個正確的路線設計,如果有偏差,就會達到不同的效果,有時候會事倍功半,反而把罪過強加到DDD領域驅動上,或者增加到聚合上,這也就是大家一直不想去更深層去研究實踐這種思想的原因。

 DDD本來就是處理復雜業務邏輯設計問題。我看到大家用DDD去分析一些小項目的時候,往往為誰是聚合根而無法達成共識這說明每個人對業務認識的角度、深度和廣度都不同,自然得出的聚合根也不同。試想,這樣的情況下,領域模型怎么保持穩定。

不過這也許不是一個大問題,只要我們用心去經營,去學習,去溝通,一切都不是問題!

 

四、結語

 今天簡單的說了下聚合,明天就正式開始項目開發,到領域服務和領域事件了,不知道你能否回答文章開頭的問題了呢?

 /// <summary>
    /// 聚合根 Order
    /// 實體有標識ID,有生命周期和狀態,通過ID進行區分
    /// 聚合根是一個實體,聚合根的標識ID全局唯一,聚合根中的實體ID在聚合根內部唯一就行
    /// 值對象主要就是值,與狀態,標識無關,沒有生命周期,用來描述實體狀態。
    /// </summary>
    /// 屬性都是值對象
    public class Order : AggregateRoot
    {
        public Guid Id;
        public string OrderNo;//值對象
        public Address Address;//值對象
        public List<OrderItem> Items;//實體集合
        //...
    }
    /// <summary>
    /// 實體 OrderItem
    /// 屬性都是值對象
    /// </summary>
    public class OrderItem : Entity
    {
        public float Price;
        public Goods Goods;
        public int Count;
        //...
    }

    /// <summary>
    /// 值對象 Goods
    /// 屬性都是值對象
    /// </summary>
    public class Goods : ValueObject
    {
        public string Name;
        //...
    }

    /// <summary>
    /// 值對象 Address
    /// </summary>
    public class Address : ValueObject
    {
        public string Country;
        public string Province;
        //...
    }
    /// <summary>
    /// 值對象
    /// </summary>
    public class ValueObject
    {

    }
    /// <summary>
    /// 領域實體
    /// </summary>
    public class Entity
    {
        public string Id;

    }
    /// <summary>
    /// 聚合根的抽象實現類,定義聚合根的公共屬性和行為
    /// </summary>
    public abstract class AggregateRoot : Entity
    {

    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM