哈嘍你們週二好,上次我們說到了實體與值對象的簡單知識,相信你們也是稍微有些瞭解,其實實體我們平時用的不少了,基本能夠和數據庫表進行聯繫,只不過值對象可能不是很熟悉,值對象簡單來講就是在DDD領域驅動設計中,爲了更好的展現領域模型之間的關係,制定的一個對象,它沒有狀態和標識,目的就是爲了表示一個值。今天呢原本不想說聚合了,由於網上的資料已經鋪天蓋地,想着開始說領域服務和領域事件了,可是爲了本系列的完整性,今天就簡單的說一下聚合和聚合根的理解,,若是你已經很明白了,請指出我說的不足之處,以即可以讓你們知道,若是你還不是很明白,請看事後思考如下幾個問題,領域事件下次再說吧,這樣也就完成了今天的頭腦風暴:數據庫
一、什麼是聚合?併發
二、聚合的做用是什麼?性能
三、咱們平時接觸到聚合了麼?學習
這裏有一個小Code,你們先看看這三者都是屬於什麼(實體,值對象,聚合/聚合根):ui
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; //... }
在DDD領域驅動設計第一次被提出的時候,聚合的概念就隨之而來了,在以前的文章中,咱們說到了領域和子領域的劃分,也說了限界上下文的定義,這些都是和咱們平時以數據模型爲中心所不一樣的概念,可能理解起來不是很容易,可是至少咱們有了這個影子,想象着一個大的領域項目,根據業務來拆分紅了多個子領域與上下文,可能不一樣的上下文中甚至有類似的概念,舉個栗子就是,訂單上下文有商品,物流上下文有貨物,庫存上下文有存貨等等等等,這時候你會發現,其實他們都是指的同一個東西,只不過在不一樣的上下文中被人爲的賦予了不一樣的概念,有的是實體(庫存),有的是值對象(訂單),可是它們又不是一個概念,由於他們屬於不一樣的子領域。spa
這個時候,既然咱們從大的方面已經對限界上下文進行分離整合,與之而來的確定是領域模型的分離(咱們確定不能把每個表放在一塊兒,也不會把他們都一個個並列排開),那既然有分離確定就是有聚合,這個時候,聚合就出來了,其實DDD提出聚合的概念是爲了保證領域內對象之間的一致性問題,由於咱們從上邊也看到了,在不一樣的地方會存在調用關係,固然主要仍是子領域內部相互調用,設計
好比建立一個訂單,必然會生成訂單詳情,訂單詳情確定會有商品信息,咱們在修改商品信息的時候,確定就不能影響到這個訂單詳情中的商品信息。再好比:用戶在下單的時候,會選擇一個地址做爲郵寄地址,若是該用戶馬上下另外一個訂單,並對本身我的中心的地址進行修改,確定就不能影響剛剛下單的郵寄地址信息。3d
這個時候,聚合就有很強的做用,經過值對象保證了對象之間的一致性。咱們平時在開發的時候,雖然沒有用到DDD,確定也是常常用到聚合,就好比上邊的問題,撇開DDD不談,就平時來講,你確定不會把商品 id 直接綁定到訂單詳情表中,爲外鍵的,否則會死得很慘。這個時候其實咱們就有一些聚合的概念了,由於什麼呢,下單的時候,咱們關注訂單領域模型,修改商品的時候,咱們關注商品領域模型,這些就是咱們說到的聚合,固然一個上下文會有不少聚合,並且聚合要儘量的細分,那如何正確的區分聚合,以及以什麼爲基準,請往下看。code
一、哪些實體或值對象在一塊兒纔可以有效的表達一個領域概念。對象
好比:訂單模型中,必須有訂單詳情,物流信息等實體或者值對象,這樣才能完整的表達一個訂單的領域概念,就好比文章開頭中提到的那個Code栗子中,OrderItem、Goods、Address等
二、肯定好聚合之後,要肯定聚合根
好比:訂單模型中,訂單表就是整個聚合的聚合根。
/// <summary> /// 聚合根 Order /// </summary> public class Order : AggregateRoot { public Guid Id; public string OrderNo; public Address Address;//值對象 public List<OrderItem> Items;//實體集合 //... }
三、對象之間是否必須保持一些固定的規則。
好比: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是一個實體。
四、聚合不要設計太大,不然會有性能問題以及業務規則一致性的問題。
對於大聚合,即使能夠成功地保持事務一致性,但它可能限制了系統性能和可伸縮性。 系統可能隨著時間可能會有愈來愈多的需求與用戶,開發與維護的成本咱們不該該忽視。
怎樣的聚合纔算是"小"聚合呢??
好的作法是使用根實體(Root Entity)來表示聚合,其中只包含最小數量的屬性或值類型屬性。哪些屬性是所需的呢??簡單的答案是:那些必須與其餘屬性保持一致的屬性。
好比,Product聚合內的name與description屬性,是須要保持一致的,把它們放在兩個不一樣的聚合顯然是不恰當的。
五、聚合中的實體和值對象應該具備相同的生命週期,並應該屬於一個業務場景。
好比一個最多見的問題:論壇發帖和回覆如何將裏聚合模型,你們想到這裏,聯想到上邊的訂單和訂單詳情,確定會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;//帖子實體聚合根 //... }
固然這樣的話,咱們就不能經過帖子一次性所有把回覆拿出來,咱們就只能單寫邏輯了,好比在應用層,可是這樣不會對領域層形成失血,由於原本就不是領域的問題。
如何聯繫,在上文的代碼中以及由體現了,這裏用文字來講明下,具體的能夠參考文中的代碼
從標識的角度:
聚合根具備全局的惟一標識,而實體只有在聚合內部有惟一的本地標識,值對象沒有惟一標識,不存在這個值對象或那個值對象的說法;
從是否只讀的角度:
聚合根除了惟一標識外,其餘全部狀態信息都理論上可變;實體是可變的;值對象是隻讀的;
從生命週期的角度:
聚合根有獨立的生命週期,實體的生命週期從屬於其所屬的聚合,實體徹底由其所屬的聚合根負責管理維護;值對象無生命週期可言,由於只是一個值;
聚合根到聚合根:經過ID關聯;
聚合根到其內部的實體,直接對象引用;
聚合根到值對象,直接對象引用;
實體對其餘對象的引用規則:1)能引用其所屬聚合內的聚合根、實體、值對象;2)能引用外部聚合根,但推薦以ID的方式關聯,另外也能夠關聯某個外部聚合內的實體,但必須是ID關聯,不然就出現同一個實體的引用被兩個聚合根持有,這是不容許的,一個實體的引用只能被其所屬的聚合根持有;
值對象對其餘對象的引用規則:只需確保值對象是隻讀的便可,推薦值對象的全部屬性都儘可能是值對象;
明確含義:一個Bounded Context(界定的上下文)可能包含多個聚合,每一個聚合都有一個根實體,叫作聚合根;
識別順序:先找出哪些實體多是聚合根,再逐個分析每一個聚合根的邊界,即該聚合根應該聚合哪些實體或值對象;最後再劃分Bounded Context;
聚合邊界肯定法則:根據不變性約束規則(Invariant)。不變性規則有兩類:1)聚合邊界內必須具備哪些信息,若是沒有這些信息就不能稱爲一個有效的聚合;2)聚合內的某些對象的狀態必須知足某個業務規則;
1.一個聚合只有一個聚合根,聚合根是能夠獨立存在的,聚合中其餘實體或值對象依賴與聚合根。
2.只有聚合根才能被外部訪問到,聚合根維護聚合的內部一致性。
其實整篇文章都是在說的聚合的優勢,這裏簡單再概況下:
聚合的出現,很大程度上,幫助了DDD領域驅動設計的所有普及,試想一下,若是沒有聚合和聚合根的思惟,單單來講DDD,總感受不是很舒服,並且領域驅動設計所分的子領域和限界上下文都是從更高的一個層面上來區分的,有的項目甚至只有一個限界上下文,那麼,聚合的思考和使用,就特別的高效,且有必要。
聚合設計的原則應該是聚合內各個有相互關聯的對象之間要保持 不變性!咱們平時設計聚合時,通常只考慮到了對象之間的關係,好比看其是否能獨立存在,是否必須依賴與某個其餘對象而存在。
我接觸的DDD中的聚合根的分析設計思路大體是這樣:一、業務本質邏輯分析;二、確認聚合對象間的組成關係;三、全部的讀寫必須沿着這些固有的路徑進行。
這是一種靜態聚合的設計思路。理論上講,彷佛沒有什麼問題。但實際上,由於每個人的思路以及學習能力,甚至是專業領域知識的不一樣,會致使設計的不合理,特別是按照這個正確的路線設計,若是有誤差,就會達到不一樣的效果,有時候會事倍功半,反而把罪過強加到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 { }