關於領域驅動設計(DDD)中聚合設計的一些思考

關於DDD的理論知識總結,可參考這篇文章。html

DDD社區官網上一篇關於聚合設計的幾個原則的簡單討論:

文章地址:http://dddcommunity.org/library/vernon_2011/,該地址中包含了一篇關於介紹如何有效的設計聚合的一些原則,共3個pdf文件。該文章中指出瞭如下幾個聚合設計的原則:node

  1. 聚合是用來封裝真正的不變性,而不是簡單的將對象組合在一塊兒;
  2. 聚合應儘可能設計的小;
  3. 聚合之間的關聯經過ID,而不是對象引用;
  4. 聚合內強一致性,聚合之間最終一致性;

上面這幾條原則,做者經過一個例子來逐步闡述。下面我按照個人理解對每一個原則作一個簡單的描述。git

聚合是用來封裝真正的不變性,而不是簡單的將對象組合在一塊兒

這個原則,就是強調聚合的真正用途除了封裝咱們自己所關心的信息外,最主要的目的是爲了封裝業務規則,保證數據的一致性。在我看來,這一點是設計聚合時最重要和最須要考慮的點;當咱們在設計聚合時,要多想一想當前聚合封裝了哪些業務規則,實現了哪些數據一致性。所謂的業務規則是指,好比一個銀行帳號的餘額不能小於0,訂單中的訂單明細的個數不能爲0,訂單中不能出現兩個明細對應的商品ID相同,訂單明細中的商品信息必須合法,商品的名稱不能爲空,回覆被建立時必需要傳入被回覆的帖子(由於沒有帖子的回覆不是一個合法的回覆),等;github

聚合應儘可能設計的小

這個原則,更多的是從技術的角度去考慮的。做者經過一個例子來講明,該例子中,一開始聚合設計的很大,包含了不少實體,可是後來發現由於該聚合包含的東西過多,致使多人操做時併發衝突嚴重,致使系統可用性變差;後來開發團隊將原來的大聚合拆分爲多個小聚合,固然,拆分爲小聚合後,原來大聚合內維護的業務規則一樣在多個小聚合上有所體現。因此實現了既能解決併發衝突的問題,也能保證讓聚合來封裝業務規則,實現模型級別的數據一致性;另外,回覆中的一位道友「殤、凌楓」提到,聚合設計的小還有一個好處,就是:業務決定聚合,業務改變聚合。聚合設計的小除了能夠下降併發衝突的可能性以外,一樣減小了業務改變的時候,聚合的拆分個數,下降了聚合大幅重構(拆分)的可能性,從而能讓咱們的領域模型更能適應業務的變化。數據庫

聚合之間經過ID關聯

這個原則,是考慮到,其實聚合之間無需經過對象引用的方式來關聯;架構

  1. 首先經過引用關聯,會致使聚合的邊界不夠清晰,若是經過ID關聯,因爲ID是值對象,且值對象正好是用來表達狀態的;因此,可讓聚合內只包含只屬於本身的實體或值對象,那這樣每一個聚合的邊界就很清晰;每一個聚合,關心的是本身有什麼信息,本身封裝了什麼業務規則,本身實現了哪些數據一致性;
  2. 若是經過引用關聯,那須要實現LazyLoad的效果,不然當咱們加載一個聚合的時候,就會把其關聯的其餘聚合也一塊兒加載,而實際上咱們有時在加載一個聚合時,不須要用到關聯的那些聚合,因此在這種時候,就給性能帶來必定影響,不過幸虧咱們如今的ORM都支持LazyLoad,因此這點問題相對不是很大;
  3. 你可能會問,聚合之間若是經過對象引用來關聯,那聚合之間的交互就比較方便,由於我能夠方便的直接拿到關聯的聚合的引用;是的,這點是沒錯,可是若是聚合之間要交互,在經典DDD的架構下,通常能夠經過兩種方式解決:1)若是A聚合的某個方法須要依賴於B聚合對象,則咱們能夠將B聚合對象以參數的方式傳遞給A聚合,這樣A對B沒有屬性上的關聯,而只是參數上的依賴;通常當一個聚合須要直接訪問另外一個聚合的狀況每每是在職責上代表A聚合須要通知B聚合作什麼事情或者想從B聚合獲取什麼信息以便A聚合本身能夠實現某種業務邏輯;2)若是兩個聚合之間須要交互,可是這兩個聚合自己只須要關注本身的那部分邏輯便可,典型的例子就是銀行轉帳,在經典DDD下,咱們通常會設計一個轉帳的領域服務,來協調源帳號和目標帳號之間的轉入和轉出,但源帳號和目標帳號自己只須要關注本身的轉入或轉出邏輯便可。這種狀況下,源帳號和目標帳號兩個聚合實例不須要相互關聯引用,只須要引入領域服務來協調跨聚合的邏輯便可;
  4. 若是一個聚合單單保存另外的聚合的ID還不夠,那是否就須要引用另外的聚合了呢?也沒必要,此時咱們能夠將當前聚合所須要的外部聚合的信息封裝爲值對象,而後本身聚合該值對象便可。好比經典的訂單的例子就是,訂單聚合了一些訂單明細,每一個訂單明細包含了商品ID、商品名稱、商品價格這三個來自商品聚合的信息;此時咱們能夠設計一個ProductInfo的值對象來包含這些信息,而後訂單明細持有該ProductInfo值對象便可;實際上,這裏的ProductInfo所包含的商品信息是在訂單生成時對商品信息的狀態的冗餘,訂單生成後,即使商品的價格變了,那訂單明細中包含的ProductInfo信息也不會變,由於這個信息已經徹底是訂單聚合內部的東西了,也就是說和商品聚合無關了。
  5. 實際上經過ID關聯,也是達到設計小聚合的目標的一種方式;

聚合內強一致性,聚合之間最終一致性

這個原則主要的背景是:若是用CQRS+Event Sourcing的架構來實現DDD,那聚合之間由於經過Domain Event(領域事件)來實現交互了,因此一樣也不須要聚合與聚合之間的對象引用,同時也不須要領域服務了,由於領域服務已經被Process(流程聚合根)和Process Manager(流程管理器,無狀態)所替代。流程聚合根,負責封裝流程的當前狀態以及流程下一步該怎麼走的邏輯,包括流程遇到異常時的回滾處理邏輯;流程管理器,無狀態。負責協調流程中各個參與者聚合根之間的消息交互,它會接受聚合根產生的domain event,而後發送command另一方面,因爲CQRS的引入,使得咱們的domain只須要處理業務邏輯,而不須要應付查詢相關的需求了,各類查詢需求專門由各類查詢服務實現;因此咱們的domain就能夠很是瘦身,僅僅只須要經過聚合根來封裝必要的業務規則(保證聚合內數據的強一致性)便可,而後每一個聚合根作了任何的狀態變動後,會產生相應的領域事件,而後事件會被持久化到EventStore,EventStore用來持久化全部的事件,整個domain的狀態要恢復,只須要經過Event Sourcing的方式還原便可;另外,當事件持久化完成後,框架會經過事件總線將事件發佈出去,而後Process Manager就能夠響應事件,而後發送新的command去通知相應的聚合根去作必要的處理;併發

上面這個過程能夠在任何一個CQRS的架構圖(包括enode的架構圖)中找到,我這裏就不貼圖了。enode中對經典的轉帳場景用這種思路實現了一下,有興趣能夠去下載enode源代碼,而後看一下其中的BankTransferSample這個例子就清楚了。另外,由於事件的響應和Command的發送是異步的,因此,這種架構下,聚合根的交互是異步的;框架

須要再次強調的一點是,聚合若是隻須要關注如何實現業務規則而不須要考慮查詢需求所帶來的好處,那就是咱們不須要在domain裏維護各類統計信息了,而只要維護各類業務規則所潛在的必須依賴的狀態信息便可;舉個例子,假如一個論壇,有版塊和帖子,之前,咱們可能會在版塊對象上有一個帖子總數的屬性,當新增一個帖子時,會對這個屬性加1;而在CQRS架構下,domain內的版塊聚合根無需維護總帖子數這個統計信息了,總帖子數會在查詢端的數據庫獨立維護;dom

從聚合和哲學的角度思考,爲何須要狀態?

聚合的角度

首先,什麼是狀態?很簡單,好比一個商品的庫存信息,那麼該庫存信息有一個商品的數量這個屬性,表示當前商品在庫存中還有多少件;那麼咱們爲何須要記錄該屬性呢?也就是爲何須要記錄這個狀態呢?由於有業務規則的存在。以這個例子爲例,由於存在「商品的庫存不能爲負數」這樣的一個業務規則,那這個規則若是要能保證,首先必須先記錄商品的庫存數量;由於商品的庫存數量是會隨着商品的賣出而減小的,而減小就是經過:Product.Count = Product.Count - 1這樣的邏輯運算來實現;這個邏輯運算要能運行的前提就是商品要有庫存信息。從這個例子咱們不難理解,一個聚合根的不少狀態,不是平白無辜設計上去的,而是某些業務規則潛在的要求,必需要設計這些狀態才能實現相應的業務規則;這樣的例子還有不少,好比銀行帳號的餘額不能小於0,致使咱們的銀行帳號必需要設計一個當前餘額的屬性;異步

另一個緣由是,看起來像是廢話,呵呵。就是:由於咱們關心這些信息,因此須要設計在當前聚合上;好比,以一個論壇的帖子爲例,做爲一個帖子,咱們一般都會關心帖子的標題、描述、發帖人、發帖時間、所屬版塊(若是論壇有版塊這個概念的話);因此,咱們就會在帖子聚合根上設計出這些屬性,以表達咱們所關心的這些信息的狀態;

哲學的角度

下面在從偏哲學的角度表達一下對象的概念吧:

人類永遠沒法認識完整的事物,由於咱們認識到的老是事物的某一方面。咱們所說的對象其實是客觀事物在人頭腦裏的反應,而事物則是不因人的認識發生改變的客觀存在。一樣一根鐵棒,在鋼材生產廠家看來,它是成品;在機械加工廠家看來,它是原料;在廢品站看來,他是商品。成品、原料、商品,這三者擁有不一樣的屬性,有本質的不一樣。爲何同一事物在不一樣人的眼裏就大相徑庭了呢?這是由於咱們老是取對咱們有用的方面來認識事物。當這根鐵棒做爲商品時,它的原料屬性依然存在,只是咱們不關心了。
 
因此,總結出來就是,由於咱們關心一個對象的某些方面,因此咱們纔會爲他設計某些狀態屬性;

關於聚合的設計的一些思考

上面只是簡單提到,聚合的設計應該多考慮它封裝了哪些業務規則這個問題。下面我想再多講一點個人一些想法:

關於GRASP九大模式中的最重要模式:信息專家模式

仍是以論壇的帖子爲例,建立一個帖子時,有一個業務規則,那就是帖子的發帖人、標題、描述、所屬板塊(若是論壇有板塊這個概念的話)都不能爲空或無效的值,由於這些信息只要有任何一個無效,那就意味着被建立出來的帖子是無效的,那就是沒有保證業務規則,也就沒辦法談領域模型的數據一致性了;若是像以往的三層貧血架構,那帖子只是一個數據的載體,不包含任何業務規則,帖子會先被構造一個空的帖子對象出來,而後咱們給這個空帖子對象的某些屬性賦值,而後保存該帖子對象到數據庫;這種設計,帖子對象只是一個數據的容器,它徹底控制不了本身的狀態,由於它的狀態都是被別人(如service)去修改的;這樣的設計,至關因而沒有把業務規則封裝在業務對象內部,而是轉移到了外部service中,雖然這樣一般也沒問題,事實上咱們大部分人都一直在這麼幹,由於這樣幹寫代碼很隨意,也很高效,呵呵。

GRASP九大模式中有一個面向對象的模式叫信息專家模式,不知道你們有了解過沒有,該模式的描述是:將職責分配給擁有執行該職責所需信息的對象;這個模式告訴咱們,若是一個對象負責維護一些信息,那它就有職責維護好這些信息。體現到對象的屬性上,那就是這個對象的屬性不能被外部隨便更改,對象本身的屬性必須本身負責維護修改。構造函數和普通的方法都會改變對象的狀態,因此,咱們對構造函數和對象普通的公共方法,都要秉持這個原則;這點很是重要,不然,若是像貧血模型那樣,那對象就不叫對象了,而只是一個普通的容納數據的容器而已,和數據庫裏的一條記錄也無本質差異了。實際上,在我看來,這也是DDD中的聚合區別於貧血模型中的實體的最大的地方。聚合不只有狀態,還有嚴格維護好本身狀態的各類方法,包括構造函數在內;而貧血模型,則只有狀態,沒有行爲;

關於DDD中一個領域對象是不是聚合根的考慮

這個問題,沒有很是清晰的放之四海而皆準的肯定方法,個人想法是:

  1. 首先從咱們對領域的最基本的常識方面的理解去思考,該對象是否有獨立的生命週期,若是有,那基本上是聚合根了;
  2. 若是領域內的一個對象,咱們會在後臺有一個獨立的模塊去管理它,那它基本上也是聚合根了;
  3. 是否有獨立的業務場景會去建立或修改一個對象;
  4. 若是對象有全局惟一的標識,那它也是聚合根了;
  5. 若是你不能肯定一個對象是不是聚合根的的時候,就先放一下,就先假定它是聚合根也無妨,而後能夠先分析一下你已經肯定的那些聚合根應該具體聚合哪些信息;也許等你分析清楚其餘的那些聚合的範圍後,也推導出了你以前不肯定是不是聚合根的那個對象是否應該是聚合根了呢。

關於一個聚合內應該聚合哪些信息的思考

  1. 把咱們所須要關心的屬性設計進去;
  2. 分析該聚合要封裝和實現哪些業務規則,從而像上面的例子(商品庫存)那樣推導出須要設計哪些屬性狀態到該聚合內;
  3. 若是咱們在建立或修改一個對象時,老是會級聯建立或修改一些級聯信息,好比在一個任務系統,當咱們建立一個任務時,可能會上傳一些附件,那這些附件的描述信息(如附件ID,附件名稱,附件下載地址)就應該被聚合在任務聚合根上;
  4. 聚合內只須要值對象和內部的實體便可,不須要引用其餘的聚合根,引用其餘的聚合根只會讓當前聚合的邊界模糊,對其餘聚合根的引用應該經過ID關聯;
  5. 聚合內的實體和值對象應該具備相同的生命週期,整個聚合是一個總體,從外部看就像是一個對象同樣,聚合應該遵循同生共死的原則;

關於如何更合理的設計聚合來封裝各類業務規則的思考

這一點在最上面的幾個原則中,實際上已經提到過一點,那就是儘可能設計小聚合,這裏的出發點主要是從技術的角度去思考,爲了下降對公共對象(大聚合)的併發修改,從而減少併發衝突的可能性,從而提升系統的可用性(由於系統用戶不會常常由於併發衝突而致使它的操做失敗);關於這一點,我還想再舉幾個例子,來講明,其實要實現各類業務規則,能夠有多種聚合的設計方式,大聚合只是其中一種;

好比,帖子和回覆,你們都知道一個帖子有多個回覆,沒有帖子,回覆就沒有意義;因此不少人就會認爲帖子應該聚合回覆;但實際上不須要這樣,若是你這樣作了,那對於一個論壇來講,同一個帖子被多我的同時回覆的可能性是很是高的,那這樣的話,多我的同時回覆一個帖子,就會致使多我的同時修改同一個帖子對象,那就致使你們都回復不了,由於會有併發衝突或者數據庫事務的等待超時,由於你們都在修改同一個帖子聚合根;實際上若是咱們從業務規則的角度去思考一下,那能夠發現,其實帖子和回覆之間,只有一個簡單的規則,那就是回覆一旦被建立,那他所對應的帖子不能被修改便可;這樣的話,要實現這個規則其實很簡單,把回覆做爲聚合根,而後把帖子傳入回覆聚合根的構造函數,而後回覆保存帖子ID,而後回覆將帖子ID設置爲不容許外部修改(private set;便可),這樣咱們就實現了這個業務規則,同時還作到了多人同時推一個帖子回覆時,不會對同一個帖子對象就併發修改,而是每一個回覆都是並行的往數據庫插入一條回覆記錄便可;

因此,經過這個例子,咱們發現,要實現領域模型內的各類業務規則,方法不止一種,咱們除了要從業務角度考慮對象的內聚關係外,還要從技術角度考慮,可是無論從什麼角度考慮,都是以實現所要求的業務規則爲前提;

從這個例子,咱們其實還發現了另一件有意義的事情,那就是一個論壇中,發表帖子和發表回覆是兩個獨立的業務場景;一我的發表了帖子,而後可能過了一段時間,另外一我的對該帖子發表了回覆;因此將帖子和回覆都設計爲獨立的很容易理解;這裏雖然帖子和回覆是一對多,回覆離開帖子確實也沒意義,可是將回復設計在帖子內沒任何好處,反而讓系統的可用性下降;相反,像上面提到的關於建立任務時同時上傳一些附件的例子,雖然一個任務也是對應多個附件信息,可是咱們發現,人物的附件信息老是隨着任務被建立或修改時,一塊兒被修改的。也就是說,咱們沒有獨立的業務場景須要獨立修改任務的某個附件信息;因此,沒有必要將任務的附件信息設計爲獨立聚合根;

ENode框架對聚合設計和聚合之間交互的支持

enode提供了一個基於DDD+CQRS+Event Sourcing+In Memory+EDA這些技術的應用開發架構;

  1. enode在框架層面就限制了一個command只能修改一個聚合根,這就杜絕了咱們使用Unit of Work的模式來以事務的方式來一次性修改多個聚合根;
  2. enode提供了可靠的原子操做和併發衝突檢測機制,來保證對單個聚合的操做的強一致性;
  3. enode提供了可靠的事件機制,來保證咱們的domain中的聚合之間數據交互能夠經過事件異步通訊的方式來實現聚合之間的最終一致性;若是有些複雜業務場景是一個流程,那咱們能夠經過Process+Process Manager的思想來實現流程狀態的跟蹤和流程的流轉;
  4. enode由於基於domain event,因此,咱們的聚合根不須要引用,每一個聚合根只須要負責本身的狀態更新,而後更新完後產生相應的domain event便可,這本質就是就是實現了:Don’t Ask, Tell這個設計原則;
  5. enode提供了可靠的事件發佈機制,能夠確保command side和query side的數據最終必定是一致的;
  6. enode提供了in memory的設計,使得咱們的domain能夠很是高效的運行,持久化事件不須要事務,獲取聚合根直接從in memory獲取;
  7. enode提供了不少設計,可讓咱們最大化的對不一樣的聚合根實例作並行操做,從而提升整個系統的吞吐量;

使用enode,將會迫使你思考如何設計聚合,如何經過流程實現聚合之間的異步交互;迫使你思考如何定義domain event,將領域內的狀態更改顯式化;迫使你將外部對領域的各類操做顯式化,即定義出各類command;迫使你將command side和query side的數據分離和架構分離,技術分離。減小的是,咱們沒必要再設計unit of work,沒必要設計domain service,沒必要讓聚合設計各類非第一手的冗餘的統計信息;

相關文章
相關標籤/搜索