應用程序框架實戰十六:DDD分層架構之值對象(介紹篇)

  前面介紹了DDD分層架構的實體,並完成了實體層超類型的開發,同時提供了驗證方面的支持。本篇將介紹另外一個重要的構造塊——值對象,它是聚合中的主要成分。數據庫

  若是說你已經在使用DDD分層架構,但你卻歷來沒有使用過值對象,這絕不奇怪,由於多年來養成的數據建模思惟已經緊緊把你禁錮,以至於你在使用面向對象方式進行開發時,仍是以數據爲中心。c#

  當咱們完成了基本的需求分析之後,若是說須要進行設計,那麼你能想到的就是數據庫表及表關係的設計,這就是數據建模。數據建模的主要依據是數據庫範式設計,根據要求嚴格程度的遞增分爲第N範式,基本的要求是把每一個標量屬性值用單獨的一列來存儲,每一個非鍵屬性必須徹底依賴於鍵屬性。數據庫範式設計的目標是消除存儲在多個位置上的冗餘數據,以避免致使更新異常。爲了達到這個目的,須要進行不斷的表拆分,直到每一個表都只表示一個單一的概念。這能夠認爲是SRP(單一職責原則)在表上的應用,從而使表中的數據產生更高的內聚性。這從數據庫的角度看多是不錯的,但對於面向對象開發卻不見得是個好事。安全

  每個表稱爲一個數據庫實體。當你完成了表設計之後,很天然的把數據庫實體與DDD實體等同起來,這產生了一個直觀的映射,因此每一個表在你的系統中都是一個實體。受這個根深蒂固的開發模式影響,你與值對象無緣相見。架構

  值對象不只在概念上提供強大的幫助,並且在技術上,特別是持久化方面可以大幅簡化系統設計,後面我將逐步介紹聚合與值對象是如何幫助你下降系統複雜性而脫困的。併發

什麼是值對象                                                  

  經過對象屬性值來識別的對象,它將多個相關屬性組合爲一個概念總體。框架

  在值對象的概念中,隱含了以下信息:異步

  1. 值對象能夠對某些簡單業務概念建模。
  2. 值對象沒有標識。值對象比實體簡單得多,不須要跟蹤變化,因此它沒有標識。
  3. 值對象是不可變的。這是值對象的核心特徵,後面將詳述。
  4. 值對象的相等性比較是經過各個屬性值的比較來完成的。
  5. 因爲值對象表明一個概念總體,因此只能進行總體替換,而不是修改值對象的某個屬性。

值對象的價值                                                  

  看了上面的概念描述,可能並不能打動你。你會說「實體不就比值對象多一個標識,能複雜到哪去」。因爲你使用實體一樣能夠對業務概念建模,因此是否使用值對象,對你來講根本不重要。數據庫設計

  下面來看看使用值對象的其它好處。性能

  值對象的一個做用是能夠幫助優化性能。當一個值對象須要在多個地方使用時,能夠共享同一個值對象。爲了共享同一個值對象,你可使用工廠來建立單例模式的值對象實例,因爲值對象是不可變的,因此能夠安全的使用。測試

  固然,你可能對使用值對象來提高性能也不感興趣,你須要更實在的好處,不然就免談。下面將介紹值對象的重型武器,它對你將產生空前的影響,甚至顛覆你平時的建模習慣和開發模式。

  前面已經說過,你爲了知足數據庫規範化設計,建立大量的表,各個表之間關係錯綜複雜,並且你也意識到正是表的膨脹致使了系統複雜性的上升。若是可以減小表的數量,那麼表之間的關係也會變得簡單和清晰,有什麼辦法能夠減小表的數量嗎?答案就是值對象與逆範式設計。

  首先來看一個簡單狀況。如今要爲人力資源系統創建員工檔案,咱們使用一個名爲Employee的員工類來表示這個業務概念,除了名字之外,還要管理他的地址信息,咱們能夠將地址信息直接放到員工實體上,數據庫表結構與員工實體同樣,代碼以下所示。

 

    /// <summary>
    /// 員工 /// </summary>
    public class Employee : EntityBase { /// <summary>
        /// 姓名 /// </summary>
        public string Name { get; set; } /// <summary>
        /// 省份 /// </summary>
        public string Province { get; set; } /// <summary>
        /// 城市 /// </summary>
        public string City { get; set; } /// <summary>
        /// 區縣 /// </summary>
        public string County { get; set; } /// <summary>
        /// 街道 /// </summary>
        public string Street { get; set; } /// <summary>
        /// 郵政編碼 /// </summary>
        public string Zip { get; set; } }

  不過你的數據庫規範化專業技能很是敏感,讓你察覺到這幾個地址屬性都不徹底依賴於員工主鍵,因此你決定專門建一張地址表,再把地址表與員工表關聯起來。

  你的代碼也做出相應調整以下。

    /// <summary>
    /// 員工 /// </summary>
    public class Employee : EntityBase{ /// <summary>
        /// 姓名 /// </summary>
        public string Name { get; set; } /// <summary>
        /// 地址編號 /// </summary>
        public Guid AddressId { get; set; } /// <summary>
        /// 地址 /// </summary>
        public Address Address { get; set; } } /// <summary>
    /// 地址 /// </summary>
    public class Address : EntityBase { /// <summary>
        /// 省份 /// </summary>
        public string Province { get; set; } /// <summary>
        /// 城市 /// </summary>
        public string City { get; set; } /// <summary>
        /// 區縣 /// </summary>
        public string County { get; set; } /// <summary>
        /// 街道 /// </summary>
        public string Street { get; set; } /// <summary>
        /// 郵政編碼 /// </summary>
        public string Zip { get; set; } }

  能夠看到,對於這樣的簡單場景,通常有兩個選擇,要麼把屬性放到外部的實體中,只建立一張表,要麼創建兩個實體,並相應的建立兩張表。第一種方法的問題是,一個總體業務概念被弱化成一堆零碎的屬性值,不只沒法表達業務語義,並且使用起來很是困難,同時將不少沒必要要的業務知識泄露到調用端。第二種方法的問題是致使了沒必要要的複雜性。

  更好的方法很簡單,就是把以上兩種方法結合起來。咱們經過把地址建模成值對象,而不是實體,而後把值對象的屬性值嵌入外部員工實體的表中,這種映射方式被稱爲嵌入值模式。換句話說,你如今的數據庫表採用上面的第一種方式定義,而你在c#代碼中經過第二種方式使用,只是把實體改爲值對象。這樣作的好處是顯而易見的,既將業務概念表達得清楚,並且數據庫也沒有變得複雜,可謂魚和熊掌兼得。

  使用嵌入值模式映射值對象,你發現將部分違反範式設計的規則,這正是數據建模與對象建模一個重要的不一樣之處。要想盡可能的發揮對象的威力,就須要弱化數據庫的做用,只把他做爲一個保存數據的倉庫。對象建模越成功,與數據建模就會差異越大。因此當違反數據庫設計原則時,不用大驚小怪,只要業務可以順利運行,就沒什麼關係。

  使用嵌入值進行映射的另外一個優點是可以優化查詢性能,由於不須要進行聯表,單表索引調優也要容易得多。

  嵌入值映射基本沒什麼反作用,它是單個值對象的標準映射方式。可是,嵌入值映射只能映射單個值對象,若是值對象是一個集合會怎樣?

  繼續咱們的員工管理模塊,客戶要求可以管理員工的教育經歷、職務變更等一系列和該員工相關的附屬信息,並且這些附屬信息都是多行記錄,好比教育經歷,他從小學一直到博士的全部教育經歷,須要屢次錄入。從數據庫的角度,就是主從表設計,員工是主表,其它都是從表。從對象的角度考慮,外層的員工是聚合根,附屬的全部信息都是聚合內部的子對象,要麼建模成實體,要麼建模成值對象,它們從概念上構成一個總體,即聚合。

  如今先來看傳統的主從表建模方式,每一個附屬信息都須要建立一個表,並映射成一個實體。若是附屬信息有10種,那麼一共須要建立11個表,能夠看到,表數據大量增長,從而致使系統變得複雜。另外,考慮員工管理在界面上的操做,能夠在界面上放一個選項卡來顯示員工的每項附屬信息,如今若是要添加員工的教育經歷,一種簡單的方法是在添加完一條教育經歷之後當即保存並刷新。但有時爲了易用性等考慮,容許客戶在界面上隨意操做,並在最後一步點擊保存按鈕一次性提交。把一個包含多個實體集合的聚合提交到服務端進行持久化,這可能很是複雜,須要從數據庫中將聚合取出,而後經過標識判斷出每一個子實體,哪些是新增的,哪些是修改的,哪些是已經刪除的。

  若是把實體換成值對象,狀況就大不相同了,將大幅簡化系統設計。前面介紹了單個值對象經過嵌入值模式映射,那麼如今是值對象集合,如何映射呢?因爲你不可能把值對象集合的每一個元素映射到外層的實體表中,可是建立多個表又增長複雜性,因此一個變態的方法是使用序列化大對象模式。把一個值對象的集合直接序列化到表中的一個字段中,這甚至違反了數據建模第一範式。能夠看到,這種保存數據的方式已經顛覆了你平時的習慣。

  說到這裏,不少人可能準備質疑這個示例的建模方案了,這些子對象能不能被建模成值對象,甚至應不該該放到員工聚合中都要看具體狀況,須要考慮多方面因素,諸如業務需求,查詢需求,併發和性能需求等,如今假設,員工的附屬信息使用值對象建模沒什麼問題,咱們來看看對系統的簡化有多大改觀。

  首先,11個表被簡化成了1個表,在表中增長了10個列而已。這個簡化簡直驚人。

  另外再來看看界面上的操做,若是須要一次性提交整個聚合,因爲值對象沒有標識,並且是總體替換的,因此你不須要從數據庫中把聚合拿出來做比較,只須要從新一個序列化,就萬事大吉。

  從上面能夠看出,值對象能夠幫你大幅簡化持久化方面的工做,這都打動不了你,我確實也無話可說。

值對象的設計要點                                                  

  值對象必須不可變。

  不變性是值對象的一個基本特徵,爲什麼要如此嚴格的規定?有幾個緣由:

  1. 值對象表明的就是一個值,這個值是一個總體,若是須要修改,必須整個替換,不能部分修改。這是從概念上說明值對象的不變性。
  2. 爲了安全的使用值對象,防止別名Bug。前面說過,值對象的一個做用是優化性能,減小內存佔用,這是經過共享同一個值對象來實現的。若是值對象容許修改,當一個值對象被多個其它對象共享時,若是其中一個對象改變了值對象的某個屬性值,這個改變在其它對象上也會立刻生效,可能致使嚴重的問題,這被稱爲別名Bug。另外,將值對象進行引用傳遞時,值對象在其它代碼中可能發生任何操做。這是從技術上保證值對象只有不可變,才能安全的使用,否則隨時可能擔憂吊膽,當發生Bug時也很難跟蹤。
  3. 當把值對象做爲Dictionary這樣的哈希集合的鍵時,哈希集合會使用值對象的GetHashCode計算出一個地址,並將值保存在這裏,以後,若是須要查找一個值,經過值對象的GetHashCode從新計算出該地址,而後把值提取出來。若是值對象是可變的,當把數據保存到哈希集合以後,修改了值對象,那麼經過值對象從新計算出來的hashcode可能不一樣,從而丟失了這個值。

  使用object建模值對象,而不是struct。

  想一想看,咱們如今討論的值對象,它的不變性與.Net提供的值類型struct如此類似,那麼是否是應該使用struct建模值對象呢?不行,緣由以下:

  1. struct用來實現基元類型,好比int,這些類型都很是小,專家建議不要超過16字節大小。咱們如今的值對象雖然比實體可能簡單些,但仍是可能很龐大。一個比較大的對象,從性能上考慮,放入堆中進行垃圾回收更合適,實際上string就是一個值對象。
  2. 若是使用像Entity Framwork這樣的ORM框架,它可能不支持struct的映射。

  嵌入值模式映射列名能夠遵循必定命名規則。

  當使用嵌入值模式進行映射時,在聚合表中,能夠根據層次關係命名列名。好比員工聚合中的地址值對象的城市屬性,能夠命名爲:Employee_Address_City,或者Address_City,這樣能夠更清晰的表達子對象的映射關係。

使用值對象的挑戰                                                  

  使用值對象的第一個挑戰來自關係數據庫。

  從上面的例子能夠看到,值對象能夠極度簡化系統設計是由於採用了序列化大對象模式。可是這種設計方式存在不少弊端,最重要的是致使搜索值對象屬性值變得異常困難。好比,客戶提出,須要根據員工教育經歷的學校名稱進行搜索,以查找哪些員工在某個學校曾經讀過。

  採用序列化大對象模式,一種方式是序列化成二進制流,而後保存到Sql Server的varbinary(MAX)字段中。若是採用這種方式存儲,當咱們要搜索教育經歷的學校名稱時,只能把全部員工讀取到內存進行過濾。除此以外,當你直接查看數據庫時,將徹底不知所云,相信你不會牛B到能讀懂二進制流的境界。還有一個問題是,當值對象的結構發生變化,好比你增長了幾個屬性,可能在反序列化時失敗。因此這種方式不被推薦。

  另外一種方式是序列化成文本流,保存到Sql Server的nvarchar(MAX)字段中。你能夠選擇XML格式,或者JSON格式。通常來說JSON要好得多,不只佔更少空間,並且更加簡單清晰。當咱們要搜索教育經歷的學校名稱時,能夠在nvarchar(MAX)字段中經過Like進行搜索,這樣雖然不是過高效,但比起讀取所有員工實體進行過濾仍是要強些。

  值對象集合的搜索解決辦法以下:

  1. 根本不提供值對象屬性的查詢條件。這一點須要你的客戶或老闆通人性才行,另外也有一些技巧。若是你直接告訴老闆,這個搜索功能作不了,你的老闆會大發雷霆「這麼簡單都作不出來,我要你來幹嗎」。可是,若是你告訴老闆不提供這幾個搜索條件,能夠提早兩天完工,他有可能就批了。
  2. 更換成NOSQL數據庫,好比MongoDB。MongoDB支持層次化存儲和查詢,從而從根本上解決問題。但不是每一個系統都能用上MongoDB,也不是每一個系統都適合使用MongoDB,好比你的系統須要很強的事務控制,但MongoDB只有一些有限的原子操做能力,不支持事務。
  3. 使用Like進行搜索,這在數據不太大的時候,也能湊活。
  4. 創建單獨的查詢數據庫或表。爲了提高查詢效率,專門爲查詢建立一些表,這些表的結構按照搜索最方便的方式設計,這樣將查詢與操做分離開來。這樣作的問題是比較麻煩,另外致使複雜度上升,但它可以兼顧操做的簡便性和查詢性能,因此也不失爲一種解決方法。使用這種方法須要將數據保存兩份,在同一事務中採用同步更新可能致使更新上的性能損失。若是採用異步方式更新,雖然性能提高,又可能致使更新延時,形成界面顯示異常等問題。
  5. 轉成實體。若是上面的方法,你以爲都很差,可能轉成實體更簡單方便。
  6. 在《實現領域驅動設計》一書中,提供了另外一種設計方案,它採用實體的表設計方式,而後在值對象的層超類型中隱藏標識,這樣在代碼中感受它仍是一個值對象,同時又可查詢。不過我我的不是太喜歡這個方案,我若是建立了單獨的表,可能使用實體更方便。

  使用值對象的另外一個挑戰來自表現層界面。

  值對象的一個關鍵設計是支持不變性,這意味着值對象的每一個屬性都沒有setter,或者setter只在對象內部容許訪問,這對咱們有什麼影響呢?

  如今你的表現層正在使用Mvc或Wpf,它們都支持模型綁定。當你在Mvc表單界面進行輸入以後,提交到控制器操做,你能夠在控制器操做上使用一個實體來接收參數。想像一下,你如今須要把員工地址傳遞到控制器操做,但因爲Address是不可變的,從而致使模型綁定失敗。

  爲了解決這個問題,使用值對象的必備條件是建立一個配套的可變值對象,對於Address,你能夠給這個可變值對象取名爲AddressViewModel,或者AddressDto都行,我通常叫它AddressInfo。這個對象的全部屬性都有setter,而且是public的,這樣才能夠在表現層使用,而後它會轉換成值對象,供領域層使用。

  從以上能夠看出,雖說考慮領域模型時,不要考慮數據庫和界面,但最終這兩個大環境對設計決策是可能形成影響的。

使用值對象的建議                                                  

  1. 聚合中儘可能使用值對象。值對象與實體在不少時候多是可互換的,因爲值對象能夠簡化系統,因此當它的缺點能夠克服就應該堅定採用。
  2. 值對象必須設計成不可變,而且值對象的任何方法都不能修改屬性值。若是值對象的方法須要進行修改,能夠經過該方法返回一個該值對象的新實例。若是對象是可變的,應該建模爲實體,而不是值對象。
  3. 若是須要跟蹤對象的生命週期,或者在聚合外部,須要進行標識引用,應該採用實體,而不是值對象。

最後,總結一下                                                  

你排斥值對象的主要緣由:

  1. 長期以來,咱們使用數據庫所形成的思惟定勢影響。
  2. 序列化大對象,形成查詢不便。
  3. 不可變值對象在界面上沒法綁定,須要額外建立配套的可變值對象,讓你以爲工做量變大。
  4. 代碼生成器沒法直接建立值對象,須要將生成出來的代碼手工調整,你不想這麼麻煩。

值對象爲你提供的主要價值:

  1. 更簡單,更清晰的表達簡單業務概念。
  2. 幫助你優化系統性能。
  3. 幫助你簡化系統設計,特別是持久化方面。

值對象的設計要點:

  1. 值對象必須不可變。
  2. 值對象的任何方法都不能直接修改屬性值,能夠經過該方法返回一個新實例。
  3. 使用object建模值對象,而不是struct。
  4. 當值對象是單個時,優先使用嵌入值模式映射。在EF中經過ComplexTypeConfiguration配置映射。
  5. 當值對象是集合,或者值對象的內部層次關係很複雜時,優先使用序列化大對象模式映射。
  6. 嵌入值模式映射列名能夠遵循必定命名規則,好比Employee_Address_City。
  7. 序列化大對象時,優先使用Json格式保存。
  8. 爲每一個值對象建立一個配套的可變值對象,以方便界面使用。

實體與值對象的區別:

  1. 實體擁有標識,而值對象沒有。
  2. 相等性測試方式不一樣。實體根據標識判等,而值對象根據內部全部屬性值判等。
  3. 實體容許變化,值對象不容許變化。
  4. 持久化的映射方式不一樣。實體採用單表繼承、類表繼承和具體表繼承來映射類層次結構,而值對象使用嵌入值或序列化大對象方式映射。

參考                                                  

  1. 若是你對映射模式感興趣,請參考《企業應用架構模式》第12章——對象關係結構模式。
  2. 若是你對別名Bug感興趣,請參考《企業應用架構模式》第18章值對象一節。
  3. 若是你對建立配套可變值對象感興趣,請參考《領域驅動設計 c# 2008實現》第97頁MutableAddress類一節。
  4. 《實現領域驅動設計》一書很是經典,建議你直接買了。

  本篇爲你們簡要介紹了值對象,下一篇咱們將完成值對象層超類型的開發。

  .Net應用程序框架交流QQ羣: 386092459,歡迎有興趣的朋友加入討論。

  謝謝你們的持續關注,個人博客地址:http://www.cnblogs.com/xiadao521/

相關文章
相關標籤/搜索