1.3.2 值对象
下面这几句话是从Domain-Driven Design:Tackling Complexity in the Heart of Software一书中摘录的(有少量改写),是笔者认为理解DDD值对象的关键点:
·用来描述领域的特定方面的、没有标识符的对象,叫作值对象。
·忽略其他类型的对象(如Service、Repository、Factory等),假设对象只有实体和值对象两种,若将那些符合实体定义的对象作为实体,那么剩下的对象就是值对象。
·推荐将值对象实现为“不可变的”(Immutable)。也就是说,值对象由一个构造器创建,并且在它们的生命周期内永远不会被修改。实现为不可变的,并且不具有标识符后,值对象就能够被安全地共享,并且能维持一致性。
对于程序员来说,还可以这么理解:如果你熟悉的语言中存在所谓的基本类型(Primitive Type),那么一般来说,它们都可以被理解为值对象。比如Java中的byte、int、long、float、boolean等,都是值对象。
Java基础类库的很多类都是值对象。比如对金额进行计算时,在Java代码中一般会使用BigDecimal来实现,而BigDecimal就是一个表示大十进制数的值对象。
BigDecimal的实现使用了两个关键的私有字段(private field),其中字段intVal的类型是BigInteger,表示“大整数”,另外一个字段scale的类型是int(整数),用来表示小数的位数。比如,一个BigDecimal对象字段intVal的值为10011,字段scale的值为2,那么这个BigDecimal对象表示的就是“100.11”。
BigInteger的实现主要依赖一个int[](整数的数组)类型的字段,通过使用这个数组可以表示任意大的整数。BigInteger也是一个值对象。
如果我们使用Java语言实现一个表示“钱”的Money值对象,内部可以用一个私有的BigDecimal类型的字段来表示金额,然后用一个String类型的字段来表示货币单位的缩写。它的构造器可能像这样:
public Money(BigDecimal amount, String currency);
当我们需要“人民币10元钱”的时候,可以按如下方式实例化一个对象:
Money tenCny = new Money(BigDecimal.valueOf(10), "CNY");
一旦这笔钱构造出来,就是一个不可变的整体。Money的加、减、乘、除,以及(按汇率)转换为以另一种货币单位表示的“钱”,都需要调用相应的方法来完成,这些方法返回的结果会是一个新的不可变的Money对象。
在Java开源社区有一个Joda Money[1]类库,其中Money类的实现就类似上面描述的形式。
我们应该尽可能地多用不可变的对象,而不是随意地为每个字段创建相应的getter/setter方法(也就是可读可写的属性)。
其实在很多领域中都需要这样的封装。举例来说,我们可能希望在开发应用时按如下方式使用值对象:
·可以声明一个属性的类型是Email,而不是String。
·可以声明一个属性的类型是手机号(MobileNumber),而不是String。
·可以声明一个属性的类型是邮政编码(PostalCode),而不是长整数(Long)。
使用过Hibernate ORM的开发人员可能会注意到在Hibernate中有一个概念是Dependent Objects(非独立的对象),可以认为它所指的就是值对象。Dependent Objects是不会被映射为数据库中的表的,它们会被映射为表中的列。
[1] 见 https://www.joda.org/joda-money/。