建立領域對象采用構造函數或者工廠,如果用工廠時需要依賴于領域服務或倉儲,則通過構造函數注入到工廠;
一個聚合是由一些列相聯的entity和value object組成,一個聚合有一個聚合根,聚合根是entity,整個聚合被看成是一個資料修改的單元,也就是說整個聚合内的所有對象要麼同時被儲存,要麼都不能儲存,即儲存到資料持久層時必須以覆寫的方式來儲存,而不是追加方式或合并的方式來儲存,否則無法確定聚合内的對象的資料一緻性。另外,整個聚合的不變性限制由聚合根負責維護。作為推導的一個結論:我們不能隻儲存一個聚合内的一部分對象;聚合内的所有實體和值對象應該總是一起被取出來一起被儲存,因為一個聚合是一個資料持久化的單元,不需要考慮将整個聚合根取出來有性能問題,因為任何一個聚合根都有明确的邊界。目前的記憶體緩存架構都已發展的比較成熟,性能已經不是問題;如mongodb,memcache,nosql,等等;
聚合内的對象之是以聚合在一起的關鍵原因不是因為它們具有一些關聯關系或依賴關系,而是因為聚合内的對象之間具有某些不變性規則,在任何時候,聚合内的所有這些對象必須滿足這些不變性規則。是以,如果一些對象之間看似有一些關聯關系或依賴關系,但是他們之間不具有任何不變性限制,那麼就不應該把這些對象放在一個聚合中,否則隻會增加這些對象之間不必要的耦合性,增加對象維護的難度;(remembering that aggregates are not about composition, but about managing invariants, we don't compose entities on an aggregate root only as a matter of convenience)。那麼為什麼一些對象之間有不變性限制後就一定非要聚合在一起不可呢?首先需要先明确一下什麼是聚合,聚合是一個整體,是修改資料的一個最小單元,一個聚合有一個頭,即聚合根,聚合根維護了整個聚合的不變性,是以整個聚合在外面看來就是一個對象,而不是多個對象的組合。另外一點非常重要,聚合在被持久化到資料庫時,是以完全覆寫的且事務的方式儲存。好了有了前面的共識之後,我們再想想為什麼聚合能保證多個對象之間的不變性規則限制?其實很隻要真正了解了前面的限制之後就很容易了解了。你想想不管一個聚合中有什麼限制,所有的限制由該聚合自己維護,是以就可以確定資料在領域模型級别就是完全一緻的,沒有任何違反規則的錯誤資料,即記憶體中的資料都是正确的。再加上這些正确的資料被持久化時是以完全覆寫的且事務的方式儲存,進而也確定了資料庫裡的資料不可能出現不一緻。這裡唯一讓你可能擔心的問題是,如果多個使用者同時更新一個聚合時,會産生并發沖突,此時将會使系統變得不可用!其實我認為這不是個問題,因為現在的支援高并發寫的分布式存儲資料庫已經非常成熟,比如淘寶的oceanbase(已經開源了),還有那些nosql也支援,或者用分布式緩存或mongodb也效率不錯。就算沒這麼好的存儲機制支援,用傳統的資料庫來存儲,我相信也不會有大問題,現在的資料庫已經不是10年前的資料庫了,在處理高并發寫的能力上已經不是同日而語了。其實并發沖突并沒有你想的那麼嚴重,一般通過select before update,以及version樂觀鎖定,就沒問題了。支付寶一天幾千萬比線上交易,全部是強一緻性,不然不叫線上交易系統。聚合根的存儲屬于單點存儲,不能用最終一緻性。最終一緻性是弱一緻性的一種特殊方式,但是最終一緻性往往用于處理分布式系統中同一份資料在多個地方有備份,然後可能會出現多個地方資料不一緻的問題,但是最終都會一緻即同步完成。具體大家可以看看cap定理。
所謂的不變性限制是指:假設有一個采購訂單order,一個order下有多個訂單項orderitem,假設有一個限制是,該采購訂單的總額不能超過100元。那麼訂單的總額不能超過100元就是一個不變性限制;那麼order和orderitem聚合在一起就顯得很有意義。在這種情況下,有order來維護這個規則,當整個訂單被儲存時,比如采用覆寫的方式儲存到資料庫。再舉個例子,比如一個論壇中有文章和回複,大家都知道一個文章有多個回複,回複離開文章沒有意義。是以大家很自然會認為文章和回複應該在一個聚合内,文章是聚合根。但是這樣其實很有問題,仔細想想會發現文章和回複之間并沒有不變性限制規則,回複和文章之間隻有一個簡單的1:n的關系而已。如果每次在添加一個回複時,都把文章先取出來,然後在文章的回複清單中把新的回複添加進去,然後再儲存整個文章,那麼不難想象,這樣做無疑是小題大做,并且每次為了更新一個回複或新增一個回複,就要把整個文章取出來,這樣做無疑非常浪費記憶體,并且在多使用者并發回同一個文章的情況下則會更糟糕。實際上仔細分析一下,文章和回複都應該是聚合,并且分别都是聚合根,我們要確定的僅僅是回複的文章不能被修改即可。添加一個回複實際上和文章無關,文章根本不關心已經有多少個回複了。這點和之前的訂單的例子不同,訂單需要準确維護其包含的所有訂單項以便能夠計算出總價是否超出100元。其實這麼多問題還是不足以詳細說明什麼樣的對象該被聚合在一起,這裡隻是作為抛磚引玉,引發大家思考如何設計聚合。
一個聚合需要具備哪些更多的特征呢?1)需要具備前面說的基本特征;2)聚合内的子對象要麼是值對象,要麼是隻讀的實體,為什麼需要隻讀,因為聚合的子實體是可以被臨時傳遞到外部的,要是外面的對象調用子對象的某個方法修改了子對象的屬性,那麼就意味着繞過聚合根修改了聚合内的東西,這樣就無法確定聚合内的不變性了;3)如果聚合根有集合類型的屬性,那麼該集合也必須是隻讀的,即不允許别人在外部添加或删除集合的元素,否則也同樣無法確定聚合的不變性。總之,我們要避免任何可能從外部修改聚合的行為發生,所有修改聚合的行為必須通過聚合根來實作。是以,理論上我們推薦大家在聚合内盡量設計值對象,原因大家多想想吧!其實從邏輯哲學的角度去思考,值對象表示了不變性,值對象表示一個值,值可以用來描述事物,事物就是實體。要是實體是由其他實體來描述,而其它實體是可變的,那麼如何確定被描述的實體是可控的?大家想想為什麼ddd書中,為什麼要在orderitem中存放當時購買時的price就知道了。要是直接引用product對象,那麼會導緻orderitem引用了一個可變的對象,就無法確定訂單的不變性限制。而唯有持久一個不變的值對象,才能維持其不變性。
evans關于聚合的兩條推薦準則:1)聚合不要設計的過大,過大的聚合很難確定不變性,進而很難確定資料的強一緻性;2)聚合與聚合之間不要通過引用的方式來關聯,而應該通過id關聯,通過id關聯也同樣能表示聚合之間的關系,并且具有更好的性能和可伸縮性,聚合根之間通過id關聯的好處是:不會因為load一個聚合根而把其他關聯的聚合根一起load出來,這樣也避免了load一個聚合根會把整個資料庫load出來的風險;另外,對orm的要求也很低,不需要orm支援lazyload;聚合根與聚合根之間的關系不像聚合内的entity之間這麼強烈内聚,它們之間僅僅是某種比較弱的關聯關系,每個聚合根都有其獨立的生命周期;
聚合内的非跟的entity以及value object之間不要互相引用,聚合内的所有child可以對根entity持有引用,如果一個child entity需要和另外一個child entity互動,則因該通過聚合根完成;
我們應該盡量減少聚合之間關聯,盡量做到單向關聯,隻保留确實需要處理的經常需要用到的周遊方向的關聯;
倉儲應了解為一個在記憶體中維護一系列聚合根的集合;
一個聚合根配備一個倉儲;
倉儲提供的接口應該總是接受聚合根或傳回聚合根,不能傳回聚合内的其他entity或value object;
不要把倉儲了解為dao,倉儲屬于領域模型的一部分,代表了領域模型向外提供接口的一部分,而dao是表示資料庫向上層提供的接口表示;
倉儲的目的不是為了支援界面查詢,不要給倉儲中設計一些目的是為了為界面提供顯示資料的接口,倉儲提供的所有接口應該僅為領域模型使用;基本的倉儲接口隻需要三個:add,remove,getbyid,其他的擴充接口可以根據業務需要擴充接口聲明;
如果一個操作僅由一個聚合根就可以完成,那麼直接調用該聚合根完成即可;
領域服務表示領域模型中的一些業務操作,這些操作通常由多個聚合根或倉儲或其他領域服務互相協作完成,那麼需要為這些操作建立領域服務,在領域服務中以過程化的方式來一步步首先根據各個聚合的id擷取到操作的相關聚合根,然後調用聚合根完成整個業務操作;比如資金轉帳,這是經典的領域服務的例子;再比如在調用某個聚合根做一個資料更新之前需要先判斷一些業務規則,但是這些判斷規則不能在該聚合根内做,因為這樣做可能會導緻聚合根依賴于外部的領域服務或倉儲,此時,應該交給領域服務來完成規則校驗和聚合根資料更新的整個過程。領域服務可以依賴倉儲或聚合根;
領域服務依賴倉儲時,工廠依賴于領域服務或倉儲時,都因該采用構造函數注入的方式,這樣可以避免領域模型中不會出現dependencyresolver.resolve<t>()這樣的語句;
切忌不要因為領域服務的引入讓聚合根變得貧血,聚合根應該有的職責還是必須要由聚合根來承擔;
聚合根内不要依賴領域服務或倉儲,如果你發現一個聚合根的職責需要依賴于某個領域服務或倉儲來幫忙完成一些其他的邏輯(像判斷業務規則之類),那麼通常你要考慮這個職責不應該由該聚合根來承擔,而應該建立合适的領域服務來承擔;聚合根的主要職責是管理其内聚的所有child entity或value object的業務完整性;
領域驅動設計時,為對象配置設定職責時,可以參考資訊專家模式:将職責配置設定給擁有執行該職責所需資訊的人;如果一個聚合根看起來擁有執行某個職責所需的資訊,但沒包含全部所需資訊,此時則不應該将該職責配置設定給該聚合根,因為強行配置設定給它,會導緻該聚合根沒有内聚性,因為勢必會依賴于其它的領域對象或領域服務或倉儲;
要學習cqrs架構,要知道我們應該将應用程式的業務邏輯處理部分(即使用者指令響應部分)和查詢部分分離;我們應該用兩個不同的技術來實作這兩個部分的實作;用ddd領域模型來實作指令部分;用最快的查詢引擎來實作查詢部分;
如果要采用cqrs架構,我們需要考慮一個成熟可靠的底層架構,否則很容易導緻指令端産生的領域對象的狀态無法同步(後者丢失)到查詢端的存儲中;
領域對象上的屬性可以具有get和set,因為我們平時所了解的對象不是真正的對象,而是某個事實的描述,比如圖書管理系統中的一個book對象,表示圖書管中放着一本書,然後該書可能有一個入庫時間。現實生活正的話,書本的入庫時間絕對不可能變化,但是軟體中的book因為不是真正的現實生活中的書本,而隻是表示圖書館中有一本書這個事實的描述,我們當然可以修改這個事實,因為我們可能因為之前在書本入庫時所輸入的入庫時間是錯的,需要修改該入庫時間,此時就有提供set的必要了。是以,理論上任何一個entity,除了id之外,其他所有屬性都可以更改,因為這些屬性并不表示現實生活中的真正對象的特征,而僅僅隻是對一個事實的描述;剛開始book對象對書本入庫這個事實的描述可能有問題,此時我們就需要修改該book的屬性;我想這個例子已經充分說明為什麼可以提供get和set了;
不要總是零散的不加任何分組的設計entity的屬性,因為有些屬性在邏輯上或業務上就是内聚的,代表一個完整的概念,比如country,province,city,town,street,等這些屬性表示一個位址的資訊,此時我們應該設計一個address對象來表示該位址資訊,此時該address就是一個值對象。是以我們在設計entity的屬性時,要好好想想,哪些子屬性其實在業務上是一個完整的概念,此時我們就需要考慮将這些相關的屬性設計為一個值對象;
切忌值對象必須是隻讀的,值對象之是以叫值對象最主要的是因為它表示一個值,而不是一個對象;值是不會變化的,是一個明确含義的不變的事物,比如3表示一個值,表述數量是3,3永遠不能變化;是以說,世界之是以存在,是因為有這些永恒不變的值對象的存在;我們隻要把值對象了解為3,“abcd”這樣的永恒不變的值就行了;
不要讓領域模型去模拟現實,模拟使用者(軟體使用者)與領域模型互動的過程;領域模型要實作的應該是使用者的需求,領域模型中不應該包含使用者的成分,想想隻有空杯子才能裝水的道理,即無為以之用的道理就明白了;是以,我們在設計領域模型時首先要明白領域模型要完成的事情是什麼;這方面,多看看用例圖,就知道軟體該做的事情了,推薦大家看的書是:craig larman寫的《uml和模式應用》一書,非常經典;