如何理解DDD中的值对象

引言

实体和值对象是领域驱动设计中的两个重要概念。相对实体而言,值对象更加抽象,理解起来也更晦涩一些。那么该如何理解值对象?我们先来看一下《实现领域驱动设计》书中对值对象的定义:

值对象 (Value Object) 是通过对象属性值来识别的对象,它将多个相关属性组合成一个概念整体,用于描述领域的某个特定方面,并且是一个没有标识符的对象。

值对象是不可变的

值对象本身也是对象,它所表达的是一个值。 那什么是值?比如:小明身高是1.45米,小红买了一支5元的笔,小王期末考试得了98分。这里的1.45米描述的是高度,5元描述的是金额、98分描述的是成绩,它们代表的都是一个值。值是一个死的东西,是没有生命的。可能你会问:人的身高不是会变化吗? 为什么说它是死的呢?这里其实有一个理解偏差,我可以这样来回答:你去体检测量身高是1.45米,那么体检表上写的就是一个冷冰冰的数字 — 1.45。如果医生不小心写错了,重新修改了这个数字,那它依旧还是一个死的东西,只不过又重新生成了一个新的值而已,这就是值的不可变性。

值对象是基于上下文的

也就是说,同样的东西在不同的业务场景中,它可以看作是实体也可以是值对象。比如:我们去超市购物,你有一张100元,我也有一张100元,我的和你的有区别吗?当然没有,只要它们的面额相同,并且都是人民币,那么它们就是一样的,此时100元就是一个值对象。但是我们知道每张人民币都是有编号的,中国人民银行可以通过编号获取这张货币的各种信息(比如:防伪标志、发行日期、流通状态等),此时100元就是一个实体,因为它需要通过ID来识别。如果有一天该货币被禁止流通了,那么它的生命周期也就结束了。 

如何定义值对象

现实中我们很容易将值对象和面向对象语言中的值类型联系起来,因为值类型天然就具备不可变性,但值对象并不完全只能用值类型来表示,也可以是引用类型。比如:一个人的姓名可以用string来表示,尽管string是一个引用类型,但具有不可变性,它就是一个单一属性的值对象。定义如下:

public string Name { get;  private set; }  // 姓名

再如:前面讲到的超市购物100元的例子,我们可以声明一个简单的 Money 类,用来表示一个具有多个属性的值对象。定义如下:

public class Money : ValueObject
{
    /// <summary>
    /// 面额
    /// </summary>
    public uint Amount { get; private set; }

    /// <summary>
    /// 币种
    /// </summary>
    public Currency Currency { get; private set; }

    public Money(uint amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}

该值对象包含了 Amount 和 Currency 两个只读属性,表示不可变。其中,Currency是一个枚举类型属性,定义如下:

public enum Currency
{
    RMB = 0,  // 人民币
    USD = 1,  // 美元
    EUR = 2,  // 欧元
}

为了实现值对象的比较,Money 类继承了 ValueObject 这个抽象类,它重写了Equals()方法,  还有相等和不等运算符(== 和 !=)。当两个相同类型的值对象,具有完全一样的属性值,那么它们就是相等的。

var money1 = new Money(100, Currency.RMB);  
var money2 = new Money(100, Currency.RMB);
Console.WriteLine(money1 == money2);  // true
Console.WriteLine(money1.Equals(money2)); // true

ValueObject 的具体实现可以参考:Implementing value objects - .NET | Microsoft Learn

最后总结

值对象的本质是一个属性集合,这些属性用于描述目的、具有整体概念且不可修改。它可以和其它值对象进行相等性比较,不过不是基于ID,而是基于值对象的属性。因为不可变的特性,它不会对协作对象带来副作用。在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免出现零碎的属性。

参考资料

DDD理论学习系列(7)-- 值对象 - 简书 (jianshu.com)

如何运用领域驱动设计 - 值对象 - 句幽 - 博客园 (cnblogs.com)

posted @ 2023-09-15 19:11  天行健君子以自强  阅读(128)  评论(0编辑  收藏  举报