剑客
关注科技互联网

值对象的威力

值对象是DDD中非常重要的一种技术,掌握这种技术让你写代码事半功倍,体会到OO的精妙。如果你是一名Java程序员,我相信你或多或少地见过值对象了,只是你没有意识到而已。

引用维基百科的 解释

In computer science, a value object is a small object that represents a simple entity whose equality is not based on identity.

字面意思就是,值对象是一个小对象,它代表着一个简单的实体,而实体的相等性不取决于它的ID。

刚刚接触OO编程的新手看完上面的解释相信直接是懵逼的,跟我接触这一概念时一样。如何理解值对象了,我还是举一个栗子。比如我们在做一个短信推送的服务,需要根据目标用户的手机号推送到相应的短信网关。我们定义了一个根据手机号推送短信的interface,很有可能我们是这么设计:

void sendMessage(String phone, String message) {
if(StringUtils.isBlank(phone) && phone.length()!=13) {
throw new IllegalArgumentException("phone format error:"+phone);
}
if(phone.starts("134")) {
sendMessageToChinaMobileGateway(phone,message);
}else if(phone.starts("130"){
sendMessageToChinaUnionGateway(phone,message);
}else if(phone.starts("189") {
sendMessageToChinaTelecomGateway(phone,message);
}else {
throw new RuntimeException("unknown phone range");
}
}

上面的过程我们只考虑3个号码段,134(移动)/130(联通)/189(电信),其他的号码短我们暂不处理。上面的处理方式有什么问题?

如果我们的工程里面只有一个地方用的 phone
的概念,也只有一个地方对 phone
所属的号码短进行判断,那么没问题。上面的写法没有任何问题,因为它是一个简单问题。但是如果你在做一个短信推送的应用,在你的工程里面会只有一个地方会使用 phone
这个概念吗,也之有一个地方需要判断号码短吗? 显然不可能。

有人可能会争论说,不就是判断号码归属吗?我可以搞一个类似 PhoneQueryService
之类的查询类,再提供一个

Operator queryBelong(String phone)
的interface不就搞定了吗? 当然,这么做也没有问题。但是当你的问题域逐渐变得复杂的时候,你就会开始有些不舒服了。因为每一个出现 phone
的地方,你发现基本上都会需要 PhoneQueryService
,但是他们在代码上又是两个东西。这种做饭的滥用最终会导致 Fat Service
的出现,代码的复用性会急剧降低。

究其原因,是因为我们把 phone
这个概念和 phone
的行为给拆开了。你可以用 String
代表任何字符类型,可以是 phone
,也可以是 name
,基本上这种类型可以代表任何东西。使用你API的人无法从中得到任何信息,除了你把变量名称叫做 phone
以外。同时,判断手机号网段这个动作是和 phone
本身强相关的,为什么不把这个动作加到 phone
里面了?! 现在,我们重构一下代码,得到类似下面的代码结构:

class Phone {
private String phoneNumber;
public Phone(String phoneNumber) {
if(!validate(phoneNumber)) {
throw new IllegalArgumentException("phone format error:"+phone);
}
this.phoneNumber = phoneNumber;
}

public static boolean validate(String phoneNmber) {
//验证逻辑
}

public boolean isMobile() {
return phoneNumber.starts("134");
}

public boolean isUnion() {
return phoneNumber.starts("130");
}

public boolean isTelecom() {
return phone.starts("189");
}

public String getRawPhone() {
return this.phoneNumber;
}

public boolean isSameWith(Phone other) {
return other!=null&&this.phoneNumber.equals(other.getRawPhone());
}
}

我们新增了一个叫 Phone
的类,并加入了判断网段归属的逻辑。引入这个类以后 sendMessage()
发生了什么变化呢?

void sendMessage(Phone phone, String message) {
checkNotNull(phone);

if(phone.isMobile()) {
sendMessageToChinaMobileGateway(phone,message);
}else if(phone.isUnion()){
sendMessageToChinaUnionGateway(phone,message);
}else if(phone.isChinaTelecom()) {
sendMessageToChinaTelecomGateway(phone,message);
}
}

咋一看,代码好像没有怎么减少啊。对于这个interface来说代码确实没有减少,反而我们还新加一个类。但是现在看看我们获得了什么:

  • 首先,方法签名变了。不在用String了,取而代之的是 Phone
    类型。这对使用者的约束更强了,我们也再也不用判断phone是否合法了。
  • 其次,判断网段归属和 phone
    合在一起了,这样我需要判断归属运营商的时候直接调用 phone
    的方法就行了。

现在,我们已经得到了一个值对象了,那就是 Phone
。它是一个小对象,代表了手机号这个概念,它的相等性是基于其业务属性的,而不是ID,而且值对象根本就没有ID这个概念。

值对象最大的好处在于增加了代码复用,同时它也是类型安全的(这一点和我之前提到了enum类似)。如果你只在一个地方使用值对象,那么你是不会体会到值对象带来的好处的。但是,每当你的代码应用一次值对象,你就会收获值对象带来的好处。
用的越多,收益越大

,这一点和单元测试比较类似。使用值对象的另外一个好处就是前置的安全校验,尤其是你在编写SDK或者开放接口的时候。因为你无法知道使用者会如何使用你的API,那么通过值对象来获得一个前置的安全校验有着非常大的好处。

值对象用在什么地方呢? 我个人的经验就是,如果在你的工程中反复出现一个具体的概念(往往跟现实生活有关),而且这个概念中涉及的行为是某种确定性的(比如你知道了手机号,就知道对应的运营商一样),那么你可以考虑一下值对象。引用《实现领域驱动设计》中关于值对象特征的定义:

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

最为重要的就是它描述了领域中的某件东西,并且它是不可变的。值对象一旦创建就不会发生变化,如果你需要表示另外一个东西,用另外一个值对象来代替它。

值对象是DDD中非常重要的部分,我们应该尽可能对使用值对象来建模,因为它是易于使用和替换的。但是值对象的实例化确实一个令人头疼的问题,尤其是聚合中存在1对多的关系时。由于这些内容涉及到DDD的多方面的知识,我不在这里展开讨论了。后续会专门讲值对象的持久化问题。之所以在讲DDD之前首先讲值对象,因为它还是少数几个可以完全脱离DDD并不失其威力的利器。就算你完全不了解DDD,也可以非常顺手的使用值对象。

说了这么多,我相信你也对值对象有个具象的认识了。纸上得来终觉浅,不如看看你现有的代码中哪些可以用值对象来代替吧!

参考文献:

Wikipedia值对象的定义

Martin Fowler值对象的解释

实现领域驱动设计

Power Use of Value Objects in DDD
: 强烈推荐

This article used CC-BY-SA-3.0 license, please follow it.

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址