值对象是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来说代码确实没有减少,反而我们还新加一个类。但是现在看看我们获得了什么:
Phone
类型。这对使用者的约束更强了,我们也再也不用判断phone是否合法了。phone
合在一起了,这样我需要判断归属运营商的时候直接调用phone
的方法就行了。
现在,我们已经得到了一个值对象了,那就是Phone
。它是一个小对象,代表了手机号这个概念,它的相等性是基于其业务属性的,而不是ID,而且值对象根本就没有ID这个概念。
值对象最大的好处在于增加了代码复用,同时它也是类型安全的(这一点和我之前提到了enum类似)。如果你只在一个地方使用值对象,那么你是不会体会到值对象带来的好处的。但是,每当你的代码应用一次值对象,你就会收获值对象带来的好处。用的越多,收益越大,这一点和单元测试比较类似。使用值对象的另外一个好处就是前置的安全校验,尤其是你在编写SDK或者开放接口的时候。因为你无法知道使用者会如何使用你的API,那么通过值对象来获得一个前置的安全校验有着非常大的好处。
值对象用在什么地方呢? 我个人的经验就是,如果在你的工程中反复出现一个具体的概念(往往跟现实生活有关),而且这个概念中涉及的行为是某种确定性的(比如你知道了手机号,就知道对应的运营商一样),那么你可以考虑一下值对象。引用《实现领域驱动设计》中关于值对象特征的定义:
最为重要的就是它描述了领域中的某件东西,并且它是不可变的。值对象一旦创建就不会发生变化,如果你需要表示另外一个东西,用另外一个值对象来代替它。
值对象是DDD中非常重要的部分,我们应该尽可能对使用值对象来建模,因为它是易于使用和替换的。但是值对象的实例化确实一个令人头疼的问题,尤其是聚合中存在1对多的关系时。由于这些内容涉及到DDD的多方面的知识,我不在这里展开讨论了。后续会专门讲值对象的持久化问题。之所以在讲DDD之前首先讲值对象,因为它还是少数几个可以完全脱离DDD并不失其威力的利器。就算你完全不了解DDD,也可以非常顺手的使用值对象。
说了这么多,我相信你也对值对象有个具象的认识了。纸上得来终觉浅,不如看看你现有的代码中哪些可以用值对象来代替吧!
参考文献:
Power Use of Value Objects in DDD: 强烈推荐
本文来自网易实践者社区,经作者蒋文康授权发布。