付出的价格,获得的是价值 ——Warren Buffett

值对象虽然经常被掩盖在实体的阴影之下,但它却是非常重要的DDD部件。

值类型用于度量和描述事物,我们可以非常容易地对值对象进行创建、测试、使用、优化和维护

我们应该尽量使用值对象来建模而不是实体对象,你可能对此非常惊讶。即便一个领域概念必须建模实体,在设计时也应该更偏向于将其作为值对象容器。这并不是源自于无端的偏好,而是因为我们可以非常容易地对值对象进行创建、测试、使用、优化和维护。

在设计的当的情况下,我们可以对值对象实例进行创建和传递,甚至在使用完之后将其直接扔掉。我们不用担心客户端对值对象的修改。一个值对象的生命周期可长可短,它就像一个无害的过客在系统中来来往往。

值对象的特征

  1. 它度量或者描述了领域中的一件东西
  2. 它可以作为不变量
  3. 它将不同的相关的属性组合成一个概念整体
  4. 当度量和描述改变时,可以用另一个值对象予以替换
  5. 它可以和其他值对象进行相等性比较
  6. 他不会对协作对象造成副作用

度量或描述

当你的模型中的确存在一个值对象时,不管你是否意识到,它都不应该成为你领域中的一件东西,而只是用于度量或描述领域中某件东西的一个概念。

一个人拥有名字,同样这里的名字不是一个实在的东西,而是描述了如何称呼这个人。

不变性

一个值对象在创建之后便不能改变了。

  1. public final class BussinessPriority {
  2. private BussinessPriorityRatings ratings;
  3. public BussinessPriority(BussinessPriorityRatings ratings) {
  4. super();
  5. this.setRatings(aRatings);
  6. this.initialize();
  7. }
  8. }

光凭初始化是不能保证值对象的不变形的。在值对象初始化之后,任何方法都不能对该对象的属性状态进行修改。在上面的例子中,只有setRatings()和initialize()方法可以修改对象的状态,而他们只有在对象构建过程中才被使用。方法setRatings()被声明为private,外界不能直接调用。

根据需要,有时我们可以在值对象中维持对实体对象的引用。在这种情况下我们需要谨慎行事。当实体对象发生改变时,引用它的值对象也将发生改变,由此违背了值对象的不变性。因此,如果实体对象有可能违背值对象的不变性,那么我们变没有理由在值对象中引用实体对象。

概念整体

一个值对象可以处理单个属性,也可以处理一组相关联的属性。在这组相关联的属性中,每一个属性都是整体属性所不可或缺的组成部分,这和简单地将一组属性组装在对象中是不同的。如果一组属性联合起来并不能表达一个整体的概念,那么这种联合并无多大用处。

可替换性

在你的模型中,如果一个实体所引用的值对象能够正确地表达其当前的状态,那么这种引用关系可以一直维持下去。否则,我们需要将整个值对象替换成一个新的值对象实例。

值对象的可替换性可以通过数字的替换性来理解。假设领域中有一个名为total的概念,该概念用整数表示。如果total的当前值被设成了3,但是之后需要重设为4,此时我们并不会将整数3修改成整数4,而是简单地将total的值重新赋值为4。

  1. FullName name = new FullName("Vaughn","Vernon");
  2. // 稍后
  3. name = new FullName("Vaughn","L","Vernon");

首先,name通过firstName和lastName进行初始化,随后name变量被替换成了另一个FullName值对象实例,该实例中包含了firstName、middleName和lastName。这里我们并没有使用FullName的某个方法来改变自身的状态,因为这样破坏了值对象的不变性。我们使用了简单的替换将另一个FullName实例的引用重新赋给了name变量。

值对象相等性

在比较两个值对象时,我们需要检查这两个值对象的相等性。在整个系统中,有可能存在很多相等的值对象实例,但他们并不表示相同的实例引用。相等性通过比较两个对象的类型和属性来决定。如果两个对象的类型和属性都相等,那么这两个对象也是相等的。进而,如果两个或多个值对象实例是相等的,我们便可以用其中一个实例来替换另一个实例。

无副作用行为

一个对象的方法可以设计成一个无副作用函数(Side-Effect-Free Function)。

这里的函数表示对某个对象的操作,它只用于产生输出,而不会修改对象的状态。由于函数执行过程中没有状态改变,这样的函数操作也称为无副作用函数。对于不变的值对象而言,所有方法都必须是无副作用函数,因为他们不能破坏值对象的不变性。

你可以将这种特性看作是不变性的一部分,但是我更倾向于将该特性从不变性中分离出来,因为这样做可以强调出值对象的一大好处。否则,我们可能只会将值对象看成一个属性容器,而忽略了值对象模式的一个功能强大的特性———-无副作用函数。

在Scrum中,我们有个实体对象Product,该对象被值对象BussinessPriority所使用:

  1. float priority = bussinessPriority.priorityOf(product);

我们至少可以看出一下问题:

  • 这里的BussinessPriority值对象不仅依赖于Product,还试图取理解该实体的内部状态。我们应该尽量地使值对象职以来它自己的属性,并且只理解他自身的状态。虽然在有些情况下这并不可行,但这是我们的目标。
  • 阅读这段代码的人并不知道使用了product的那些部分。这种表达方式并不明确,从而降低了模型的清晰度。更好的方式是只传入需要用到的product属性。
  • 更重要的是,在将实体作为参数的值对象方法中,我们很难看出该方法是否会对实体进行修改,测试也将变得非常困难。因此,即便一个值对象承诺不会修改实体,我们也很难证明这一点。
    1. float priority = bussinessPriority.priority(product.bussinessPriorityTotals());

最小化集成

在所有的DDD项目中,通常存在多个限界上下文,这意味着我们需要找到合适的方法对这些上下文进行集成。当模型概念从上游上下文流入下游上下文中时,尽量使用值对象来表示这些概念。这样的好处是可以达到最小化集成,既可以最小化下游模型中用于管理职责的属性数目。使用不变得值对象使得我们做更少的职责假设。

image.png

用值对象表示标准类型

在许多应用和系统中,都会使用到标准类型(standard type)。

标准类型是用于表示事物类型的描述性对象。系统中既有表示事物的实体和描述实体的值对象,同时还存在标准类型来区分不同的类型。

你的通用语言定义了一个PhoneNumber值对象,同时需要为每个PhoneNumber对象制定一个类型。“这个号码是家庭电话、移动电话、工作电话还是其他类型的电话号码?”不同类型的电话号码类型需要建模成一种类写层级关系吗?为每个类型创建一个类对于客户端使用来说是非常困难的。此时,你需要的是使用标准类型来描述不同类型的电话号码,比如Home、mobile、work或者other。