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


DDD理論學習系列——案例及目錄


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; 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; }
    }
}

很簡單的類,我想你在沒了解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就具有了值的特征,我們可以直接使用Address address = new Address("廣東省", "深圳市", "南山區", "高新科技園中區一路 ", "518057");)來表示一個具體的通過屬性識別的不可變的位置概念。在DDD中,我們稱這個Address為值對象。讀到這里,你可能會覺得值對象也不過如此,也可能會有一堆問題,但請稍安勿躁,我們繼續講解。

4.DDD中的值對象

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

4.1.值對象的特征

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

  • 描述了領域中的一件東西
  • 不可變的
  • 將不同的相關屬性組合成了一個概念整體
  • 當度量和描述改變時,可以用另外一個值對象予以替換
  • 可以和其他值對象進行相等性比較
  • 不會對協作對象造成副作用

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

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

4.2.值對象的問題

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

  1. 單個值對象
    上面我們提到值對象不會孤立存在,所以我們可以將值對象中的屬性作為所屬實體/聚合根的數據列來存儲(比如,我們可以將收貨地址的屬性映射到客戶實體中)。這樣做就會導致數據表列數增多,但是能夠優化查詢性能,因為不需要聯表查詢。
  2. 多個值對像序列化到單個列
    當每個客戶僅允許維護一個收貨地址時,我們用上面的方式沒有問題。但很顯然一個客戶可以有多個收貨地址。這個時候我們該怎么持久化值對象集合呢?不可能把值對象集合的每個元素映射到外層的實體表中,但是創建多個表又增加復雜性,所以一個變態的方法是使用序列化大對象模式。把一個集合序列化后塞到外層實體表的某一列中,是有點匪夷所思。而且數據庫的列寬是有限制的,且不方便查詢。但似乎也帶來一個好處,大大簡化了系統的設計(不用設計多列分別存儲了)。
  3. 使用數據庫實體保存多個值對像
    使用層超類型來賦予值對象一個委派標識,以數據庫實體的形式保存值對象。(關於層超類型,可參考我上一篇文章,這里不作贅述。)

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

4.3.值對象的作用

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

  • 符合通用語言,更簡單明了的表達簡單業務概念。
  • 提升系統性能。
  • 簡化設計,減少不必要的數據庫表設計。

5.建模值對象

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

6. 總結

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


參考資料
應用程序框架實戰十六:DDD分層架構之值對象(介紹篇)
DDD領域驅動設計(二) 之 值對象
值對象的威力


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM