领域设计:Entity与VO

  • 2020 年 12 月 2 日
  • 笔记

本文探讨如下内容:

  • 什么是状态
  • 什么是标识
  • 什么是Entity
  • 什么是VO(ValueObject)
  • 在设计中如何识别Entity和VO

要理解Entity和VO,需要先理解两个概念:“状态”和“标识”!我们先来聊聊“状态”!

状态

大家肯定都在淘宝买过东西吧!在淘宝购买商品后,会有一个订单,记录了你购买的商品信息、价格、店铺信息、还有一个特别重要的信息,就是订单状态。通过这个订单状态,我们可以知道我们的购物流程现在进行到哪一步了。如果你犹豫了很久才下定决心购买了一件心仪已久的商品,你是不是很在意订单状态?时不时要刷新一下页面,看看订单状态是否显示已送达了?

开发过系统的都知道,一般订单状态都是使用一个字段来表示的,比如status,不同的状态就是给status赋不同的值。但是这个status就是“订单状态”吗?难道状态就是一个字段?!

Order{
 product
 location
 seller
 buyer
 status
 ...
}

你有没有想过,当我们说“状态”的时候,我们实际上指的是什么?

我们在很多场景下会用到“状态”这个词,比如:

  • 你今天“状态”不错哦
  • 朋友又发朋友圈“状态”了
  • 我在淘宝买的商品已经是发货“状态”了
  • REST(表述性状态转移)中的状态

以“你今天状态不错”这句为例,如果状态就是一个字段!那么,“你今天状态不错”就是status=1?!“你今天状态不行”就是status=0?!很明显,这不合理!

如果“状态”不是简单的一个字段的话,那么“状态”到底是什么呢?

其实在架构风格:你真的懂REST吗?已经提过了!文中对REST的解释,有这么一句:一个由网页组成的网络(一个虚拟状态机),用户通过选择链接在应用中前进(状态迁移),导致下一个页面(应用的下一个状态的表述)被转移给用户,并且呈现给他们,以便他们来使用。

结合上面的几个场景,你有没有发现,“状态”实际上表示的是“目标对象在当前时刻所呈现出的内容”!在软件系统中通过一个字段来表示状态只是一种简化手段!

如无特殊说明,下面所提到的“状态”指的是“目标对象在当前时刻所呈现出的内容”,而不是指状态字段

  • 你今天“状态”不错哦:你今天给人的感觉很好
  • 朋友又发朋友圈“状态”了:朋友圈当前的内容
  • 我在淘宝买的商品已经是发货“状态”了:你的购物流程目前所在的环节
  • REST(表述性状态转移)中的状态:当前呈现在用户面前的页面

既然“状态”表示的是“当前时刻所呈现出的内容”!那么说明了“状态”是个快照/瞬态!也就是说,“目标对象”有多个“状态”,“当前状态”只是“目标对象”众多“状态”中的一个!

大家应该玩过定格动画吧?就像下面这样(下图截自《大侦探福尔摩斯2:诡影游戏》):

领域设计:Entity与VO

 

图中的小册子就是“目标对象”,册子的每一页就是“状态”,当前展示出来的那一页就是“当前状态”!

在理解了什么是“状态”以后,我们就可以来初步区分Entity和VO了:

  • Entity在整个生命周期中,有多个“状态”,也就是说“状态”是可变的(至于变不变就看实际情况了)
  • 而VO在整个生命周期中,只有一个“状态”,也就是说“状态”不变

现在,问题又来了,对于VO来说,因为“状态”是不可变的,我们就可以用其“状态”来表示VO!但是对于Entity来说,因为有多个“状态”,且“状态”是可变的,那我们如何来表示呢?以上面的Order为例,假设同一个买家在同一个卖家那里买了两个同样的商品,那两个订单里的信息都是一样的,但是它是两个不同的订单,我们如何区分这两个订单呢?

现在就轮到下一个主角登场了:“标识”!

标识

说到“标识”,我们最先想到的是编程语言中的“引用”或“指针”!比如下面的代码:

Order orderA = new Order("productA",...);
Order orderB = new Order("productA",...);
orderA.productName = "productB";
  • 前面两行,orderA和orderB虽然订单信息(状态)都相同,但是这是两个不同的订单
  • 第三行,即使改了orderA的产品名称(状态),依然还是相同的订单

这解决了“区分相同状态的不同Entity”的问题,但是没有解决Entity有多个状态的问题。因为“标识”指向的是目标对象的当前状态。而且,很多编程语言中有个很大的问题,就是不区分“标识”和“状态”!什么意思呢?

假设我们在看一部电影,当我们开始观看时,就是这部电影生命周期的开始,观看结束就是这部电影生命周期的结束,在这段时间里,电影的画面(状态)一帧帧的呈现在我们面前,我们可以通过播放、快进、后退、暂停改变电影的状态,每个状态都是相互独立的,类似这样:

领域设计:Entity与VO

 

随着时间的改变,我们能获取到电影的不同状态,每个状态是相互独立的。但是实际上我们的代码逻辑像下面这样:

var movie1 = new Movie();
movie1.setCurrentFrame("第三帧");
var currentMovie = movie1
movie1.setCurrentFrame("第四帧");
currentMovie // 还是第三帧吗?

电影播放到第三帧,我们用一个变量currentMovie保存了电影的当前状态(第三帧),但是后面电影播放第四帧了,currentMovie也就变成了第四帧的状态了。

语言中的这种“标识”(我称为“隐式标识”)还有另外一个问题,就是无法跨系统。比如,在分布式系统中,需要保证两个系统中的对象是同一个对象,这种“隐式标识”是做不到的。

所以“隐式标识”并不能满足我们的需求。我们需要“显示标识”,“显示标识”在现实中很常见:

  • 每个人都有身份证,即使有两个人名字相同、性别一样、身材相同、甚至整容了样貌都一样,但是身份证号码是不一样的,身份证号码就是每个人的“显示标识”
  • 一个产品线上生产的产品可以说一模一样,但是都会有一个唯一的产品编号,这个产品编号就是产品的“显示标识”

在上面购物的列子中,就相当于给Order一个唯一标识,比如一个唯一的订单号:

Order{
 orderNo // 显示标识
 product
 location
 seller
 buyer
 status
 ...
}

给定订单号以后,无论订单的状态如何变化,只要订单号不变,那么它就是同一个订单。

所以,“标识”是另一个区分Entity和VO的关键点:

  • Entity有标识
  • 而VO没有标识

注意标识并不一定只是一个字段,可能是多个字段的组合,这需要根据不同的业务逻辑来确定。比如在一个学校系统里,可以通过学年+班级+学号来标识一个学生。

Entity和VO

理解了标识和状态,我们就可以来定义Entity和VO了:

  • Entity是具有多个“状态”的对象,“状态”在其生命周期中可能会改变,通过“标识”来唯一确定这个对象
  • VO只有一个“状态”,且是在创建时就确定的,也就是说VO是不可变的

现在我们知道了什么是Entity,什么是VO,那么我们如何在系统中识别哪些对象是Entity,哪些对象又是VO呢?

如何识别Entity和VO

一个对象是表示成Entity还是VO,取决于系统的关注点。

我们还以淘宝购物为例,假设你在某家店铺买了个商品,质量很好。过了一段时间后,你想再买一个,但是你记不得是哪家店了,于是你从已完成的订单列表中点击商品想进去再次购买。但是你点进去后发现,商品下架了。

这是因为“商品”在“订单系统”中是个VO,而在“商品管理系统”中是Entity!其实很好理解:

  • 在“商品管理系统”中,系统需要关注“商品”的“状态”,需要维护是否上架、库存多少、各种属性等信息(多种状态)。就是说在“商品管理系统”中,商品状态是可变的。所以它也有“标识”,即商品ID
  • 而“订单系统”并不关心“商品”的“状态”变化,它只关注在创建订单时,这个“商品”的当前“状态”是什么,并且在订单创建完成后,这个“商品”的“状态”就不会再改变了

在“商品管理系统”中,商品可以这样表示:

Product {
 id // 商品标识
 name
 desc
 status
 ...
}

而在“订单系统”中,订单是个Entity,商品是个VO,可以这么表示:

Order{
 orderNo // 订单标识
 product:Product
 status
 ...
}
Product {
 id // 这里不是标识,只是状态
 name
 desc
 status
 ...
}

注意这里的id并不是标识,这里的id实际上退化成了状态的一部分,保留这个id是为了和“商品管理系统”进行交互,通过id从商品管理系统中查询商品。当然还有其它方式,例如保存“商品管理系统”中该商品的历史URL。

总结

本文从对“状态”和“标识”的理解开始,一步步来解释什么是Entity和VO,以及如何在系统中识别Entity和VO。后面将进一步讨论Entity与VO的关系,以及与其它组件的关系,例如DTO,Service,Resporitory,DAO等

参考资料

  • 《领域驱动设计:软件核心复杂性应对之道》
  • 《实现领域驱动设计》
  • 《Clojure编程乐趣》
  • 《七周七并发模型》