前面介紹了DDD分層架構的實體,並完成了實體層超類型的開發,同時提供了驗證方面的支持。本篇將介紹另外一個重要的構造塊——值對象,它是聚合中的主要成分。數據庫
若是說你已經在使用DDD分層架構,但你卻歷來沒有使用過值對象,這絕不奇怪,由於多年來養成的數據建模思惟已經緊緊把你禁錮,以至於你在使用面向對象方式進行開發時,仍是以數據爲中心。c#
當咱們完成了基本的需求分析之後,若是說須要進行設計,那麼你能想到的就是數據庫表及表關係的設計,這就是數據建模。數據建模的主要依據是數據庫範式設計,根據要求嚴格程度的遞增分爲第N範式,基本的要求是把每一個標量屬性值用單獨的一列來存儲,每一個非鍵屬性必須徹底依賴於鍵屬性。數據庫範式設計的目標是消除存儲在多個位置上的冗餘數據,以避免致使更新異常。爲了達到這個目的,須要進行不斷的表拆分,直到每一個表都只表示一個單一的概念。這能夠認爲是SRP(單一職責原則)在表上的應用,從而使表中的數據產生更高的內聚性。這從數據庫的角度看多是不錯的,但對於面向對象開發卻不見得是個好事。安全
每個表稱爲一個數據庫實體。當你完成了表設計之後,很天然的把數據庫實體與DDD實體等同起來,這產生了一個直觀的映射,因此每一個表在你的系統中都是一個實體。受這個根深蒂固的開發模式影響,你與值對象無緣相見。架構
值對象不只在概念上提供強大的幫助,並且在技術上,特別是持久化方面可以大幅簡化系統設計,後面我將逐步介紹聚合與值對象是如何幫助你下降系統複雜性而脫困的。併發
經過對象屬性值來識別的對象,它將多個相關屬性組合爲一個概念總體。框架
在值對象的概念中,隱含了以下信息:異步
看了上面的概念描述,可能並不能打動你。你會說「實體不就比值對象多一個標識,能複雜到哪去」。因爲你使用實體一樣能夠對業務概念建模,因此是否使用值對象,對你來講根本不重要。數據庫設計
下面來看看使用值對象的其它好處。性能
值對象的一個做用是能夠幫助優化性能。當一個值對象須要在多個地方使用時,能夠共享同一個值對象。爲了共享同一個值對象,你可使用工廠來建立單例模式的值對象實例,因爲值對象是不可變的,因此能夠安全的使用。測試
固然,你可能對使用值對象來提高性能也不感興趣,你須要更實在的好處,不然就免談。下面將介紹值對象的重型武器,它對你將產生空前的影響,甚至顛覆你平時的建模習慣和開發模式。
前面已經說過,你爲了知足數據庫規範化設計,建立大量的表,各個表之間關係錯綜複雜,並且你也意識到正是表的膨脹致使了系統複雜性的上升。若是可以減小表的數量,那麼表之間的關係也會變得簡單和清晰,有什麼辦法能夠減小表的數量嗎?答案就是值對象與逆範式設計。
首先來看一個簡單狀況。如今要爲人力資源系統創建員工檔案,咱們使用一個名爲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個列而已。這個簡化簡直驚人。
另外再來看看界面上的操做,若是須要一次性提交整個聚合,因爲值對象沒有標識,並且是總體替換的,因此你不須要從數據庫中把聚合拿出來做比較,只須要從新一個序列化,就萬事大吉。
從上面能夠看出,值對象能夠幫你大幅簡化持久化方面的工做,這都打動不了你,我確實也無話可說。
不變性是值對象的一個基本特徵,爲什麼要如此嚴格的規定?有幾個緣由:
想一想看,咱們如今討論的值對象,它的不變性與.Net提供的值類型struct如此類似,那麼是否是應該使用struct建模值對象呢?不行,緣由以下:
當使用嵌入值模式進行映射時,在聚合表中,能夠根據層次關係命名列名。好比員工聚合中的地址值對象的城市屬性,能夠命名爲:Employee_Address_City,或者Address_City,這樣能夠更清晰的表達子對象的映射關係。
使用值對象的第一個挑戰來自關係數據庫。
從上面的例子能夠看到,值對象能夠極度簡化系統設計是由於採用了序列化大對象模式。可是這種設計方式存在不少弊端,最重要的是致使搜索值對象屬性值變得異常困難。好比,客戶提出,須要根據員工教育經歷的學校名稱進行搜索,以查找哪些員工在某個學校曾經讀過。
採用序列化大對象模式,一種方式是序列化成二進制流,而後保存到Sql Server的varbinary(MAX)字段中。若是採用這種方式存儲,當咱們要搜索教育經歷的學校名稱時,只能把全部員工讀取到內存進行過濾。除此以外,當你直接查看數據庫時,將徹底不知所云,相信你不會牛B到能讀懂二進制流的境界。還有一個問題是,當值對象的結構發生變化,好比你增長了幾個屬性,可能在反序列化時失敗。因此這種方式不被推薦。
另外一種方式是序列化成文本流,保存到Sql Server的nvarchar(MAX)字段中。你能夠選擇XML格式,或者JSON格式。通常來說JSON要好得多,不只佔更少空間,並且更加簡單清晰。當咱們要搜索教育經歷的學校名稱時,能夠在nvarchar(MAX)字段中經過Like進行搜索,這樣雖然不是過高效,但比起讀取所有員工實體進行過濾仍是要強些。
值對象集合的搜索解決辦法以下:
使用值對象的另外一個挑戰來自表現層界面。
值對象的一個關鍵設計是支持不變性,這意味着值對象的每一個屬性都沒有setter,或者setter只在對象內部容許訪問,這對咱們有什麼影響呢?
如今你的表現層正在使用Mvc或Wpf,它們都支持模型綁定。當你在Mvc表單界面進行輸入以後,提交到控制器操做,你能夠在控制器操做上使用一個實體來接收參數。想像一下,你如今須要把員工地址傳遞到控制器操做,但因爲Address是不可變的,從而致使模型綁定失敗。
爲了解決這個問題,使用值對象的必備條件是建立一個配套的可變值對象,對於Address,你能夠給這個可變值對象取名爲AddressViewModel,或者AddressDto都行,我通常叫它AddressInfo。這個對象的全部屬性都有setter,而且是public的,這樣才能夠在表現層使用,而後它會轉換成值對象,供領域層使用。
從以上能夠看出,雖說考慮領域模型時,不要考慮數據庫和界面,但最終這兩個大環境對設計決策是可能形成影響的。
本篇爲你們簡要介紹了值對象,下一篇咱們將完成值對象層超類型的開發。
.Net應用程序框架交流QQ羣: 386092459,歡迎有興趣的朋友加入討論。
謝謝你們的持續關注,個人博客地址:http://www.cnblogs.com/xiadao521/