本文探讨如下内容:

  • 什么是状态
  • 什么是标识
  • 什么是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在整个生命周期中,只有一个「状态」,也就是说「状态」不变

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

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

标识

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

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

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

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

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

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

领域设计:Entity与VO的更多相关文章

  1. .net ef core 领域设计代码转换(上篇)

    一.前言 .net core 2.0正式版已经发布几个月了,经过研究,决定把项目转移过来,新手的话可以先看一些官方介绍 传送门:https://docs.microsoft.com/zh-cn/dot ...

  2. Java Bean、POJO、 Entity、 VO 、PO、DAO

    Java Bean.POJO. Entity. VO , 其实都是java 对象,只不过用于不同场合罢了.    Java Bean: 就是一个普通的Java 对象, 只不过是加了一些约束条件.  声 ...

  3. 当实体类中entity/DTO/VO等类中,有枚举值,应该怎么输出?

    当实体类中entity/DTO/VO等类中,有枚举值,应该怎么输出? 问题: orderStatus 和 payStatus都是枚举类,并且枚举的个数达地10来个,我们不可能在模板页面(jsp/ftl ...

  4. 领域设计之模型充血、Repository对象注入

    工作中接触了不少项目组,他们在实际的项目开发中,Domain Object的贫血模型设计,还是主要的应用的范式.原因在于,贫血模型模型设计中,把所有涉及持久化的业务逻辑,封装到了Domain Serv ...

  5. NewQuant的设计(一)——整体的领域设计

    NewQuant的设计思路——整体的领域分析 “领域驱动设计(DDD)”是著名软件工程建模专家Eric Evans提出的一个重要概念,是“面向对象分析设计(OOAD)”的深化.当业务逻辑变得复杂,系统 ...

  6. java架构之项目结构(entity / DTO / VO)

    定义类的讲究 关系示例 定义类的讲究 ejb Enterprise JavaBean(EJB),企业javaBean.是java的核心代码,分别是会话Bean(Session Bean).实体Bean ...

  7. [转]领域驱动设计系列文章(2)——浅析VO、DTO、DO、PO的概念、区别和用处

    原文地址:http://www.blogjava.net/johnnylzb/archive/2010/05/27/321968.html 上一篇文章作为一个引子,说明了领域驱动设计的优势,从本篇文章 ...

  8. 领域驱动设计系列文章——浅析VO、DTO、DO、PO的概念、区别和用处

    本篇文章主要讨论一下我们经常会用到的一些对象:VO.DTO.DO和PO. 由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概念进行一个简单描述,名字只是个标识,我们重点关注其概念: 概念: ...

  9. 领域驱动设计系列(2)浅析VO、DTO、DO、PO的概念、区别和用处

    PO:persistant object持久对象 最形象的理解就是一个PO就是数据库中的一条记录.好处是可以把一条记录作为一个对象处理,可以方便的转为其它对象. BO:business object业 ...

随机推荐

  1. kube-proxy实现原理

    1.service概念 service是一组pod的服务抽象,相当于一组pod的LB,负责将请求分发给对应的pod.service会为这个LB提供一个IP,一般称为cluster IP.kube-pr ...

  2. [BZOJ 2287/POJ openjudge1009/Luogu P4141] 消失之物

    题面: 传送门:http://poj.openjudge.cn/practice/1009/ Solution DP+DP 首先,我们可以很轻松地求出所有物品都要的情况下的选择方案数,一个简单的满背包 ...

  3. How to Convert and Import VHD to VMDK (VMWare)

    VHD or Virtual Hard Disk is the disk image format used by Microsoft virtualization software such as ...

  4. 【SpringCloud】03.微服务的设计原则

    微服务的设计原则: 一.AKF拆分原则 业界对于可扩展的系统架构设计有一个朴素的理念:通过加机器就可以解决容量和可用性问题(如果一台不行就两台). Y轴(功能)--关注应用中功能划分,基于不同的业务拆 ...

  5. Elasticsearch 注册windows服务后,服务启动失败,意外终止

    直接双击elasticsearch.bat可以成功启动,注册成服务后就启动失败 从网上查找问题,发现是jdk版本的问题,用ES自带的jdk就可以启动成功. 默认ES会先找JAVA_HOME环境变量,如 ...

  6. 21 Ajax

    21 Ajax AJAX,Asynchronous JavaScript and XML(异步的 JavaScript 和 XML), 是与在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页 ...

  7. Python如何快速复制序列?

    1 基本用法 把序列乘以一个整数,就会产生一个新序列.这个新序列是原始序列复制了整数份,然后再拼接起来的结果. l=[1,2,3] l2=l * 3 logging.info('l2 -> %s ...

  8. 【RabbitMQ-7】RabbitMQ—交换机标识符

    死信队列概念 死信队列(Dead Letter Exchange),死信交换器.当业务队列中的消息被拒绝或者过期或者超过队列的最大长度时,消息会被丢弃,但若是配置了死信队列,那么消息可以被重新发布到另 ...

  9. impala-shell -o a.txt 查询中有中文时报错问题的处理

    当使用impala-shell -o a.txt进入impala-shell之后,查询报错: 报错情况: Query: select * from dim_sales_dept Unknown Exc ...

  10. linux netfilter nat2

    linux netfilter nat1 后面在上传