本文探讨如下内容:

  • 什么是状态
  • 什么是标识
  • 什么是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. python爬虫scrapy框架

    Scrapy 框架 关注公众号"轻松学编程"了解更多. 一.简介 Scrapy是用纯Python实现一个为了爬取网站数据.提取结构性数据而编写的应用框架,用途非常广泛. 框架的力量 ...

  2. 【QT】 QThread部分源码浅析

    本文章挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的.其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进 ...

  3. dubbo2.7.X版本带来的服务注册和服务调用方式改变

    参考地址:https://www.cnblogs.com/alisystemsoftware/p/13064620.html 注册中心数据结构格式改变(service:接口服务,application ...

  4. 想springboot启动完成后执行某个方法

    如题,很多时候,我们都需要在springboot项目启动后初始化化一些自己的数据 原文地址:https://www.jianshu.com/p/f80f833ab8f6 实现方法有2个. 一.Appl ...

  5. boston.csv 完整版 508个数据集

    https://pan.baidu.com/s/1C1Llx8cTu5xBdK9GuDZ11A 提取码:u6cm

  6. ssh连接客户端一段时间没响应就断掉的解决办法-保持长连接

    修改(添加)server端的 /etc/ssh/sshd_config #server每隔60秒发送一次请求给client,然后client响应,从而保持连接 ClientAliveInterval ...

  7. 安全也挺让人心烦的 ---login shell

    今天查问题时, ssh 登录后台发现 需要输入密码,输入密码后弹出一个二维码, 然后扫码获取秘钥.输入秘钥登陆!!! 真是恶心了一把,找手机都花了不少时间!!!! 那么怎样干掉输入密码这些操作呢??? ...

  8. 使用qmake编译时拷贝文件

    使用qmake构建项目时,需要在make时拷贝一些文件到指定位置,非执行make install,下面总结列举一下我了解的方式 COPIES 示例pro 1 COPY_DIR = $$PWD/copy ...

  9. matlab 向量操作作业

    写出下列语句的计算结果及作用 clear    清除所有变量 clc    清屏 A = [2 5 7 1 3 4];    创建行向量并赋值 odds = 1:2:length(A);    冒号操 ...

  10. js常用通用函数

    //判断是否数字 function isNumber(str){ var n = Number(str); if (!isNaN(n)){ return true; }else{ return fal ...