实现
我是喜欢这个BusinessPrioriy示例的,因为它向我们展示了值对象的所有特征。除了展示不变性、概念整体.可替换性、相等性和无副作用行为之外,BusinessPriority还向我们展示了如何将值对象用作策略模式[Gamma et al.]。
有了以上的测试.团队成员们知道了客户端是如何使用BusinessPriority对象的.他们按照测试中的断言实现了BusinessPriority 类:
public final class BusinessPriority implements Serializable {
private static final long serialVersionUID = 1L;
private BusinessPriorityRatings ratings;
public BusinessPriority(BusinessPriorityRatings aRatings) {
super();
this.setRatings(aRatings);
}
public BusinessPriority(BusinessPriority aBusinessPriority) {
this(aBusinessPriority.ratings());
}
}
团队成员决定使该值对象实现Serializable接口。有时.一个值对象实例是需要序列化的.比如当和某个远程系统通信时。另外.当需要对值对象持久化时,序列化也是有帮助的。
BusinessPriority自身维护了一个类型为BusinessPriorityRatings的值对象属性ratings。该属性描述了一个待定项的业务价值.它向BusinessPriority提供了benefit, cost, penalty和risk等排名信息.使得我们可以在BusinessPriority上进行不同类型的计算。
通常来说,我至少都会为值对象创建两个构造函数。第一个构造函数接受用于构建对象状态的所有属性参数,它是主要的构造函数。该构造函数首先初始化默认的对象状态,对于基本属性的初始化通过调用私有的setter方法实现。该私有的setter方法向我们展示了一种自委派性,这是我所推荐的。
保持值对象的不变性
只有主构造函数才能使用自委派性来设置属性值,除此之外,其他方法都不能使用 setter方法。由于值对象中的所有setter方法都是私有的,消费方是没有机会直接调用这些setter方法的。这是保持值对象不变性的两个重要因素。
第二个构造函数用于将一个值对象复制到另一个新的值对象,即复制构造函数。该构造函数采用浅复制(Shallow Copy)的方式,因为它也是将构造过程委派给主构造函数的,先从原对象中取出各个属性值,再将这些属性值作为参数传给主构造函数。当然,我们也可以采用深复制(Deep Copy)或者克隆(done)的方式,即为每个所引用的属性都创建一份其自身的备份。然而,这种方式既复杂,也没有必要。当需要深度复制时,我们才考虑添加该功能。但是对于不变的值对象来说,在不同的实例间共享属性是不会出现什么问题的。
复制构造函数对于单元测试来说是非常重要的。在测试值对象时,我们希望验证它的不变性。就像前面所展示的一样,在单元测试开始时,创建一个值对象实例,并通过复制构造函数创建该实例的一份备份,同时验证这两个实例的相等性。接下来,测试值对象的无副作用行为方法。如果所有的验证都通过了,最后我们需要验证这两个实例依然是相等的。
现在,我们来实现值对象的策略部分:
public float costPercentage(BusinessPriorityTotals aTotals) {
return (float) 100 * this.ratings().cost() / aTotals.totalCost();
}
public float priority(BusinessPriorityTotals aTotals) {
return this.valuePercentage(aTotals) / (this.costPercentage(aTotals) + this.riskPercentage(aTotals));
}
public float riskPercentage(BusinessPriorityTotals aTotals) {
return (float) 100 * this.ratings().risk() / aTotals.totalRisk();
}
public float totalValue() {
return this.ratings().benefit() + this.ratings().penalty();
}
public float valuePercentage(BusinessPriorityTotals aTotals) {
return (float) 100 * this.totalValue() / aTotals.totalValue();
}
public BusinessPriorityRatings ratings() {
return this.ratings;
}
有些计算方法需要一个类型为BusinessPriorityTotals的参数。该值对象提供了有关待定项成本和风险的总体描述,它对于计算某个待定项的优先级百分比来说是有必要的。这些行为都不会改变实例的自身状态。在各个行为方法执行完毕之后,我们都会在测试中验证状态的不变性。
在当前的策略模式中,并不存在独立接口(Fowler,P of EAA),因为此时只有一种实现。在将来,可能会有更多的实现,比如该敏捷项目管理软件可能会让用户自己提供优先级计算算法,每种算法策略都对应着各自的实现。
这些无副作用方法的名字是重要的。虽然所有的方法都返回值对象(因为它们都是CQS查询方法),但是团队成员故意没有使用JavaBean的命名规范,即为方法加上“get”前缀。这种方式使得代码与通用语言保持一致。使用getValuePercentage()只是一个技术上的用法,但是valuePercentage()则是一种流畅的、可读的语言表达。
流畅的Java表达式到哪里去了?
我认为JavaBean规范对于对象设计来说存在负面影响.同时它也没有体现出领域驱动设计的原则。让我们来看看JavaBean规范之前的那些API。比如java,lang.String, String类中只有为数不多的查询方法使用了get前缀。多数查询方法的命名都是非常流畅的.比如charAt(), compareTof). concat(). contains(), endsWith(),indexOf(), length(). replace(>、startsWithf), substring()等。这里并没有多少JavaBean的坏味道。当然.只是这一个例子是不能说明问题的。然而.JavaBean规范出来之后的API的确缺少了很多流畅性。一种流畅的.可读的语言表达方式是值得拥有的。
对于一些使用了JavaBean规范的工具.我们是有解决办法的。比如.Hibernate支持字段级别的访问(对象属性).因此在使用Hibernate时.我们可以根据自己的需要来命名方法的名字.而不会对持久化造成影响。
然而.对于其他的_些工具.要使用富有表达性的接口可能就会有问题了。比如在使用Java EL或OGNL时.你很有可能得不到期望的结果。当然,我们还可以使用其他的方式.比如数据传输对象(Data Transfer Object, DTO) [Fowler. P ofEAA].该对象提供了getter方法,它将值对象的属性通过getter方法暴露给用户界面。DTO是一个常用的模式.但是从技术上来说并没有多大必要.因此.DTO也不见得是最好的选择。此时.我们可以考虑使用展现模型(Presentation Model).我们将在应用程序(14)中对此做详细讲解,由于展现模型实现了适配器模式[Gamma et al.].它可以向使用EL的视图层提供gettei* 方法。如果以上方法都失败了.那么你不得不回到原地,乖乖地在领域对象中使用getter方法。
即便如此.在设计值对象时.我们也不应该完全地遵循JavaBean规范。比如,JavaBean规范要求我们提供公有的setter方法,而这将违背值对象的不变性特征。
下面一组方法包含了标准的equals()、hashCode()和toStringO方法:
@Override
public boolean equals(Object anObject) {
boolean equalObjects = false;
if (anObject != null && this.getClass() == anObject.getClass()) {
BusinessPriority typedObject = (BusinessPriority) anObject;
equalObjects = this.ratings().equals(typedObject.ratings());
}
return equalObjects;
}
@Override
public int hashCode() {
int hashCodeValue = + (169065 * 179) + this.ratings().hashCode();
return hashCodeValue;
}
@Override
public String toString() {
return "BusinessPriority" + " ratings = " + this.ratings();
}
这里的equalsO方法用于检查不同值对象的相等性。通常来说,在比较相等性时,我们将省略掉对非null的检查。传人的参数对象必须与当前对象具有相同的类型。在类型相同时,equalsO方法会对两个对象所有的属性进行比较,当它们之间每组对应的属性都相等时,两个整体值对象则相等。
根据Java标准,hashCode()方法和equalsO方法拥有相同的契约,即如果两个对象是相等的,那么它们的hashCodeO方法也应该返回相同的结果。
对于toStringO方法来说,并没有什么特别之处。它为值对象的状态创建一条人类可读的描述信息。根据需要,你可以对该描述信息进行格式化。BusinessPriority还剩下几个方法:
protected BusinessPriority() {
super();
}
private void setRatings(BusinessPriorityRatings aRatings) {
if (aRatings == null) {
throw new IllegalArgumentException(
"The ratings are required.");
}
this.ratings = aRatings;
}
无参数构造函数是为一些框架准备的,比如Hibernate。由于该构造函数总是隐藏起来的,我们没有必要担心客户端会使用该构造函数来创建非法对象实例。在构造函数和setter/getter被隐藏的情况下,Hibernate依然可以工作。这个无参数的构造函数使得Hibernate或其他工具能够对对象进行重建,比如重建保存在持久化存储中的对象实例。这些工具首先通过无参数构造函数初始化一个空对象,再通过setter方法向该空对象中填人属性值。对于Hibernate,我们可以将其配置成直接设置属性值,而不用使用setter方法。这里的BusinessPriority便是如此,我们并没有完全遵循JavaBean规范。需要注意的是,客户端使用的是公有的构造函数,而不是这个隐藏的构造函数。
最后,BusinessPriority提供了对ratings的setter方法。这里我们可以看到自封装/自委派的一大好处。一个对象的setter和getter方法并不见得只局限于设置对象的属性值,还可以进行断言[Evans]操作,这对于通常的软件开发和DDD模型来说都是很重要的。
对参数的合法性进行断言称为守卫,此时的断言保护着对象,使其处于一种合法的状态。守卫断言能够,并且应该用于任何有可能接受错误参数的地方。在本例中,setRatings()方法首先检查所传人的aRatings参数是否为null,如果是,则抛出Illegal ArgumentException 异常。诚然,该 setter 方法在一个 BusinessPriority实例的生命周期中只会使用一次,但是这里的守卫断言依然是有用的。你还会在别处看到自委派的好处。特别地,实体(5) —章在讨论验证时对此做了详细的讲解。