DDD理論學習系列(7)-- 值對象

[DDD理論學習系列——案例及目錄:http://www.jianshu.com/p/6e2917551e63]


1.引言

提到值對象,我們可能立馬就想到值類型和引用類型。而在C#中,值類型的代表是strut和enum,引用類型的代表是class、interface、delegate等。值類型和引用類型的區別,大家肯定都知道,值類型分配在棧上,引用類型分配在堆上。 那是不是值類型對應的就是值對象,引用類型對應的就是實體嗎?很抱歉,不是的。

值對象我們要分開來看,其包含兩個詞:值和對象。值是什麼?比如,數字(1、2、3.14),字符串(“hello world”、“DDD”),金額(¥50、$50),地址(深圳市南山區科技園)它們都是一個值,這個值有什麼特點呢,固定不變,表述一個具體的概念。對象又是什麼?一切皆爲對象,是對現實世界的抽象,用來描述一個具體的事物。那值對象=值+對象=將一個值用對象的方式進行表述,來表達一個具體的固定不變的概念

所以瞭解值對象,我們關鍵要抓住關鍵字——

2.值的特徵

1就是代表數字1,“Hello DDD”就是一個固定字符串,“¥50”就是表示人民幣50元。假設你手上有一沓鈔票,我們去超市購物的時候,很顯然我們會根據面額去付款,不會拿20元當50元花,也不會把美元當人民幣花,畢竟¥50≠$50。那對於鈔票來說,我們怎麼識別它們,無非就是鈔票上印刷的數字面額和貨幣單位。你可能會說了,每張鈔票上都印有編號,就算同樣面額的毛爺爺,那它也不一樣。這個陳述,我竟然無言以對。但我只想問你,你平時購物付款,是用編號識別面額的啊?編號顯然是銀行關心的事,與我們無關。 我們這裏提到的數字面額、貨幣單位和編號,除此之外還有發行日期,其實都是鈔票的基本特徵,在coding中我們會根據場景選擇性的對某些特徵以屬性的形式加以抽象。而在我們日常消費的場景下,顯然編號和發行日期這兩個特徵我們可以直接忽略不計。

從上面這個例子我們可用總結出值的特徵:

  1. 表示一個具體的概念

  2. 通過值的屬性對其識別

  3. 屬性判等

  4. 固定不變

3.案例分析

購物網站都會維護客戶收貨地址信息來進行發貨處理,一個地址信息一般主要包含省份、城市、區縣、街道、郵政編碼信息。

如果要讓我們設計,我們肯定噼裏啪啦就把代碼寫下來了:

    /// <summary>
    /// 地址
    /// </summary>
    public class Address {
        /// <summary>
        ///Id
        /// </summary>
        public int AddressId{ get; private set; }
        /// <summary>
        /// 省份
        /// </summary>
        public string Province { get; private set; }
        /// <summary>
        /// 城市
        /// </summary>
        public string City { get; private set; }
        /// <summary>
        /// 區縣
        /// </summary>
        public string County { get; private set; }
        /// <summary>
        /// 街道
        /// </summary>
        public string Street { get; private set; }
        /// <summary>
        /// 郵政編碼
        /// </summary>
        public string Zip { get; private set; }
    }
}

很簡單的類,我想你在沒了解DDD值對像之前肯定會這樣寫,這並不奇怪,我之前也是這樣設計的,爲了將Address映射到數據庫,我們需要定義一個AddressId作爲主鍵映射,這是數據建模的結果。那在DDD中應該如何設計?別急,我們一步一步的分析。

首先,我們要問自己一個問題,地址是什麼?廣東省深圳市南山區高新科技園中區一路 郵政編碼: 518057(騰訊大廈),它就是一個標準的地址,表述的是一個具體的不變的位置信息。它不會隨着時間而變化,它包含了地址所需要的完整屬性(省份、城市、區縣、街道、郵政編碼)信息。所以,地址是一個值。

按照我們現在的設計,如果有多個所處騰訊大廈的註冊用戶,我們數據庫將存在多條相同的地址信息(只是Id不同)。但Id不同,就不是同一個地址嗎?我們在做發貨處理的時候,難道會因爲Id不同,而將貨物發往不同的地方嗎?很顯然不是的。這也再次論證了地址是一個值的事實。

那我們如何抽象設計這個地址呢,讓其具有值的特徵? 我們一條一條的來進行分析。

  1. 表示一個具體的概念 我們上面設計的Address類,也能表示出地址這個概念。

  2. 通過值的屬性對其識別 也就是不需要唯一標識,刪去我們設計的AddressId即可。

  3. 屬性判等 重寫Equals方法,比較屬性判斷。

  4. 固定不變 就是通過構造函數來初始化,所有屬性均不提供修改入口。

修改後的Address如下:

   /// <summary>
/// 地址
/// </summary>
public class Address
{
    /// <summary>
    /// 省份
    /// </summary>
    public string Province { get; private set; }
    /// <summary>
    /// 城市
    /// </summary>
    public string City { get; private set; }
    /// <summary>
    /// 區縣
    /// </summary>
    public string County { get; private set; }
    /// <summary>
    /// 街道
    /// </summary>
    public string Street { get; private set; }
    /// <summary>
    /// 郵政編碼
    /// </summary>
    public string Zip { get; private set; }
    public Address(string province, string city,
        string county, string street, string zip)
    {
        this.Province = province;
        this.City = city;
        this.County = county;
        this.Street = street;
        this.Zip = zip;
    }
    public override bool Equals(object obj)
    {
        bool isEqual = false;
        if (obj != null && this.GetType() == obj.GetType())
        {
            var that = obj as Address;
            isEqual = this.Province == that.Province
                && this.City == that.City
                && this.County == that.County 
                && this.Street == that.Street 
                && this.Zip == that.Zip;
        }
        return isEqual;
    }
    public override int GetHashCode()
    {
        return this.ToString().GetHashCode();
    }
    public override string ToString()
    {
        string address = $"{this.Province}{this.City}" +
            $"{this.County}{this.Street}({this.Zip})";
        return address;
    }
}

至此,我們的 Address就具有了值的特徵,我們可以直接使用 Addressaddress=newAddress("廣東省","深圳市","南山區","高新科技園中區一路 ","518057");)來表示一個具體的通過屬性識別的不可變的位置概念。在DDD中,我們稱這個 Address爲值對象。讀到這裏,你可能會覺得值對象也不過如此,也可能會有一堆問題,但請稍安勿躁,我們繼續講解。

4.DDD中的值對象

通過上面對值的特徵分析,結合實際的案例,我們設計出了一個 Address這個值對象。那在DDD中對值對象又是怎樣描述的呢?

4.1.值對象的特徵

咱們來看看《實現領域驅動設計》上是如何定義的吧:

  • 描述了領域中的一件東西

  • 不可變的

  • 將不同的相關屬性組合成了一個概念整體

  • 當度量和描述改變時,可以用另外一個值對象予以替換

  • 可以和其他值對象進行相等性比較

  • 不會對協作對象造成副作用

由此可見,值對象包含了值所具有的全部特徵。

另外有一點:個人認爲值對象不會孤立的存在,它有其所屬。比如我們所說的地址,它是一個客觀存在。沒有一個具體的上下文語境,它就僅僅是一個字符串。只有在某個具體的領域下,纔有其實質意義,比如客戶收貨地址、售後地址。

4.2.值對象的問題

說到問題,你可能想到的第一個問題就是持久化的問題。是的,值對象沒有標識列如何存儲數據庫呢? 當下比較流行使用ORM持久化機制,使用ORM將每個類映射到一張數據庫表,再將每個屬性映射到數據庫表中的列會增加程序的複雜性。那如何使用ORM持久化來避免這一問題呢?

  1. 單個值對象 上面我們提到值對象不會孤立存在,所以我們可以將值對象中的屬性作爲所屬實體/聚合根的數據列來存儲(比如,我們可以將收貨地址的屬性映射到客戶實體中)。這樣做就會導致數據表列數增多,但是能夠優化查詢性能,因爲不需要聯表查詢。

  2. 多個值對像序列化到單個列 當每個客戶僅允許維護一個收貨地址時,我們用上面的方式沒有問題。但很顯然一個客戶可以有多個收貨地址。這個時候我們該怎麼持久化值對象集合呢?不可能把值對象集合的每個元素映射到外層的實體表中,但是創建多個表又增加複雜性,所以一個變態的方法是使用序列化大對象模式。把一個集合序列化後塞到外層實體表的某一列中,是有點匪夷所思。而且數據庫的列寬是有限制的,且不方便查詢。但似乎也帶來一個好處,大大簡化了系統的設計(不用設計多列分別存儲了)。

  3. 使用數據庫實體保存多個值對像 使用層超類型來賦予值對象一個委派標識,以數據庫實體的形式保存值對象。(關於層超類型,可參考我上一篇文章,這裏不作贅述。)

你可能會覺得第3個方法好,因爲其更符合傳統的設計方式,但其並非DDD推崇的一種方式,因爲層超類型讓值對象有了實體的影子。在進行持久化設計的時候,我們要謹記根據領域模型來設計數據模型,而不是根據數據模型來設計領域模型

4.3.值對象的作用

通過上面的分析介紹,我們可以體會到值對象帶來的以下好處:

  • 符合通用語言,更簡單明瞭的表達簡單業務概念。

  • 提升系統性能。

  • 簡化設計,減少不必要的數據庫表設計。

5.建模值對象

值對象作爲領域建模工具之一,有其存在的意義。領域中,並不是每一個事物都必須有一個唯一身份標識,對於某些對象,我們更關心它是什麼而無需關心它是哪個。所以建模值對象,我們關鍵要結合通用語言的表述看其是否有值的含義和特徵

6. 總結

如果非要對值對象進行總結的話,我希望你記住我開頭的那句話:值對象=值+對象=將一個值用對象的方式進行表述,來表達一個具體的固定不變的概念。 仔細揣摩,定有收穫。


參考資料 [應用程序框架實戰十六:DDD分層架構之值對象(介紹篇):http://www.cnblogs.com/xiadao521/p/4121861.html] [DDD領域驅動設計(二) 之 值對象:http://www.cnblogs.com/fan-yuan/p/3513867.html] [值對象的威力:http://michael-j.net/2016/10/18/%E5%80%BC%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%A8%81%E5%8A%9B/]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章