领域设计: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编程乐趣》
  • 《七周七并发模型》