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

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


1.引言

提到值對象锨侯,我們可能立馬就想到值類型和引用類型嫩海。而在C#中,值類型的代表是strut和enum囚痴,引用類型的代表是class出革、interface、delegate等渡讼。值類型和引用類型的區(qū)別骂束,大家肯定都知道,值類型分配在棧上成箫,引用類型分配在堆上展箱。
那是不是值類型對應的就是值對象,引用類型對應的就是實體嗎蹬昌?很抱歉混驰,不是的。

值對象我們要分開來看皂贩,其包含兩個詞:值和對象栖榨。值是什么?比如明刷,數(shù)字(1婴栽、2、3.14)辈末,字符串(“hello world”愚争、“DDD”),金額(¥50挤聘、$50)轰枝,地址(深圳市南山區(qū)科技園)它們都是一個值,這個值有什么特點呢组去,固定不變鞍陨,表述一個具體的概念。對象又是什么从隆?一切皆為對象诚撵,是對現(xiàn)實世界的抽象缭裆,用來描述一個具體的事物。那值對象=值+對象=將一個值用對象的方式進行表述砾脑,來表達一個具體的固定不變的概念幼驶。

所以了解值對象艾杏,我們關鍵要抓住關鍵字——韧衣。

2.值的特征

1就是代表數(shù)字1,“Hello DDD”就是一個固定字符串购桑,“¥50”就是表示人民幣50元畅铭。假設你手上有一沓鈔票,我們去超市購物的時候勃蜘,很顯然我們會根據(jù)面額去付款硕噩,不會拿20元當50元花,也不會把美元當人民幣花缭贡,畢竟¥50≠$50炉擅。那對于鈔票來說,我們怎么識別它們阳惹,無非就是鈔票上印刷的數(shù)字面額和貨幣單位谍失。你可能會說了,每張鈔票上都印有編號莹汤,就算同樣面額的毛爺爺快鱼,那它也不一樣。這個陳述纲岭,我竟然無言以對抹竹。但我只想問你,你平時購物付款止潮,是用編號識別面額的扒耘小?編號顯然是銀行關心的事喇闸,與我們無關兢孝。
我們這里提到的數(shù)字面額、貨幣單位和編號仅偎,除此之外還有發(fā)行日期跨蟹,其實都是鈔票的基本特征,在coding中我們會根據(jù)場景選擇性的對某些特征以屬性的形式加以抽象橘沥。而在我們日常消費的場景下窗轩,顯然編號和發(fā)行日期這兩個特征我們可以直接忽略不計。

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

  1. 表示一個具體的概念
  2. 通過值的屬性對其識別
  3. 屬性判等
  4. 固定不變

3.案例分析

購物網(wǎng)站都會維護客戶收貨地址信息來進行發(fā)貨處理座咆,一個地址信息一般主要包含省份痢艺、城市仓洼、區(qū)縣、街道堤舒、郵政編碼信息色建。

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

    /// <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>
        /// 區(qū)縣
        /// </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映射到數(shù)據(jù)庫陵吸,我們需要定義一個AddressId作為主鍵映射,這是數(shù)據(jù)建模的結果介牙。那在DDD中應該如何設計壮虫?別急,我們一步一步的分析环础。

首先囚似,我們要問自己一個問題,地址是什么线得?廣東省深圳市南山區(qū)高新科技園中區(qū)一路 郵政編碼: 518057(騰訊大廈)饶唤,它就是一個標準的地址,表述的是一個具體的不變的位置信息框都。它不會隨著時間而變化搬素,它包含了地址所需要的完整屬性(省份、城市魏保、區(qū)縣熬尺、街道、郵政編碼)信息谓罗。所以粱哼,地址是一個值。

按照我們現(xiàn)在的設計檩咱,如果有多個所處騰訊大廈的注冊用戶揭措,我們數(shù)據(jù)庫將存在多條相同的地址信息(只是Id不同)。但Id不同刻蚯,就不是同一個地址嗎绊含?我們在做發(fā)貨處理的時候,難道會因為Id不同炊汹,而將貨物發(fā)往不同的地方嗎躬充?很顯然不是的。這也再次論證了地址是一個值的事實。

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

  1. 表示一個具體的概念
    我們上面設計的Address類伴找,也能表示出地址這個概念盈蛮。
  2. 通過值的屬性對其識別
    也就是不需要唯一標識,刪去我們設計的AddressId即可技矮。
  3. 屬性判等
    重寫Equals方法抖誉,比較屬性判斷。
  4. 固定不變
    就是通過構造函數(shù)來初始化穆役,所有屬性均不提供修改入口寸五。

修改后的Address如下:

   /// <summary>
/// 地址
/// </summary>
public class Address
{
    /// <summary>
    /// 省份
    /// </summary>
    public string Province { get; private set; }

    /// <summary>
    /// 城市
    /// </summary>
    public string City { get; private set; }

    /// <summary>
    /// 區(qū)縣
    /// </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("廣東省", "深圳市", "南山區(qū)", "高新科技園中區(qū)一路 ", "518057");)來表示一個具體的通過屬性識別的不可變的位置概念。在DDD中韧拒,我們稱這個Address為值對象淹接。讀到這里,你可能會覺得值對象也不過如此叛溢,也可能會有一堆問題塑悼,但請稍安勿躁,我們繼續(xù)講解楷掉。

4.DDD中的值對象

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

4.1.值對象的特征

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

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

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

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

4.2.值對象的問題

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

  1. 單個值對象
    上面我們提到值對象不會孤立存在腔彰,所以我們可以將值對象中的屬性作為所屬實體/聚合根的數(shù)據(jù)列來存儲(比如,我們可以將收貨地址的屬性映射到客戶實體中)辖佣。這樣做就會導致數(shù)據(jù)表列數(shù)增多霹抛,但是能夠優(yōu)化查詢性能,因為不需要聯(lián)表查詢卷谈。
  2. 多個值對像序列化到單個列
    當每個客戶僅允許維護一個收貨地址時杯拐,我們用上面的方式?jīng)]有問題。但很顯然一個客戶可以有多個收貨地址世蔗。這個時候我們該怎么持久化值對象集合呢端逼?不可能把值對象集合的每個元素映射到外層的實體表中,但是創(chuàng)建多個表又增加復雜性污淋,所以一個變態(tài)的方法是使用序列化大對象模式顶滩。把一個集合序列化后塞到外層實體表的某一列中,是有點匪夷所思寸爆。而且數(shù)據(jù)庫的列寬是有限制的礁鲁,且不方便查詢。但似乎也帶來一個好處赁豆,大大簡化了系統(tǒng)的設計(不用設計多列分別存儲了)仅醇。
  3. 使用數(shù)據(jù)庫實體保存多個值對像
    使用層超類型來賦予值對象一個委派標識,以數(shù)據(jù)庫實體的形式保存值對象歌憨。(關于層超類型着憨,可參考我上一篇文章,這里不作贅述务嫡。)

你可能會覺得第3個方法好甲抖,因為其更符合傳統(tǒng)的設計方式,但其并非DDD推崇的一種方式心铃,因為層超類型讓值對象有了實體的影子准谚。在進行持久化設計的時候,我們要謹記根據(jù)領域模型來設計數(shù)據(jù)模型去扣,而不是根據(jù)數(shù)據(jù)模型來設計領域模型柱衔。

4.3.值對象的作用

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

  • 符合通用語言,更簡單明了的表達簡單業(yè)務概念唆铐。
  • 提升系統(tǒng)性能哲戚。
  • 簡化設計,減少不必要的數(shù)據(jù)庫表設計艾岂。

5.建模值對象

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

6. 總結

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


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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末响蓉,一起剝皮案震驚了整個濱河市硕勿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枫甲,老刑警劉巖源武,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異想幻,居然都是意外死亡粱栖,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門脏毯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闹究,“玉大人,你說我怎么就攤上這事食店≡伲” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵吉嫩,是天一觀的道長价认。 經(jīng)常有香客問我,道長自娩,這世上最難降的妖魔是什么用踩? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上脐彩,老公的妹妹穿的比我還像新娘碎乃。我一直安慰自己,他們只是感情好惠奸,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布荠锭。 她就那樣靜靜地躺著,像睡著了一般晨川。 火紅的嫁衣襯著肌膚如雪证九。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天共虑,我揣著相機與錄音愧怜,去河邊找鬼。 笑死妈拌,一個胖子當著我的面吹牛拥坛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼醋旦,長吁一口氣:“原來是場噩夢啊……” “哼缺前!你這毒婦竟也來了?” 一聲冷哼從身側響起著摔,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎定续,沒想到半個月后谍咆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡私股,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年摹察,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倡鲸。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡供嚎,死狀恐怖,靈堂內的尸體忽然破棺而出峭状,到底是詐尸還是另有隱情克滴,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布宁炫,位于F島的核電站偿曙,受9級特大地震影響,放射性物質發(fā)生泄漏羔巢。R本人自食惡果不足惜望忆,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一罩阵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧启摄,春花似錦稿壁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蕾羊,卻和暖如春喧笔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背龟再。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工书闸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人利凑。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓浆劲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哀澈。 傳聞我的和親對象是個殘疾皇子牌借,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內容