浅析领域驱动设计中的VO、DTO、DO、PO的概念、区别和用处。
前言
之前给自己立了一个Flag,也就是在12月底学完领域驱动设计,目前基础的知识都基本学完了,就差一个完整的Demo,打算还是通过两篇文章完善DDD的基础知识,等DDD的Demo写完后,在出1-2篇文章去讲解这个Demo,这个系列就结束了。
上一篇DDD的文章《一文带你学习DDD,全是干货!》,里面包含了DDD的重要基本概念,想学习DDD的同学,可以先看看这篇文章。
概念介绍
- VO(View Object):视图对象,主要对应界面显示的数据对象。对于一个WEB页面,或者SWT、SWING的一个界面,用一个VO对象对应整个界面的值。
- BO(business object):业务对象,主要作用是把业务逻辑封装为一个对象。这个对象可以包括一个或多个其它的对象。比如一个简历,有教育经历、工作经历、社会关系等等。我们可以把教育经历对应一个PO,工作经历对应一个PO,社会关系对应一个PO。建立一个对应简历的BO对象处理简历,每个BO包含这些PO。这样处理业务逻辑时,我们就可以针对BO去处理。
- DTO(Data Transfer Object):数据传输对象,主要用于远程调用等需要大量传输对象的地方。比如我们一张表有100个字段,那么对应的PO就有100个属性。但是我们界面上只要显示10个字段,客户端用WEB service来获取数据,没有必要把整个PO对象传递到客户端,这时我们就可以用只有这10个属性的DTO来传递结果到客户端,这样也不会暴露服务端表结构.到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为VO。在这里,我泛指用于展示层与服务层之间的数据传输对象。
- POJO(plain ordinary java object) :简单java对象,个人感觉POJO是最常见最多变的对象,是一个中间对象,也是我们最常打交道的对象。一个POJO持久化以后就是PO,直接用它传递、传递过程中就是DTO,直接用来对应表示层就是VO。(POJO、PO、DTO、VO都是处理流程中的名字,不是PO对应一个POJO,DTO对应一个POJO,VO对应一个POJO在有些情况下PO、DTO、VO是指同一个POJO)
- DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
- PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。最形象的理解就是一个PO就是数据库中的一条记录,好处是可以把一条记录作为一个对象处理,可以方便的转为其它对象。
上面的基础概念可,通过这个简图简单描述一下,可能不全,仅供参考:
数据转换
以一个时序图建立简单模型,来描述对象在三层架构应用中的位置:
- 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO。
- 展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。
- 服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。
- 服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作。
- 对于一个逆向操作,如读取数据,也是用类似的方式转换和传递,略。
谈谈具体区别&应用
VO与DTO的区别
大家可能会有个疑问:既然DTO是展示层与服务层之间传递数据的对象,为什么还需要一个VO呢?对!对于绝大部分的应用场景来说,DTO和VO的属性值基本是一致的,而且他们通常都是POJO,因此没必要多此一举。但不要忘记这是实现层面的思维,对于设计层面来说,概念上还是应该存在VO和DTO,因为两者有着本质的区别,DTO代表服务层需要接收的数据和返回的数据,而VO代表展示层需要显示的数据。
举个例子:例如服务层有一个getUser的方法返回一个系统用户,其中有一个属性是gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。说到这里,可能你还会反驳,在服务层直接就返回“帅哥美女”不就行了吗?对于大部分应用来说,这不是问题,但设想一下,如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。再者,回到设计层面上分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的DTO,不应该出现与表现形式的耦合。
个人观点:VO和DTO还是有必要的,至少会做到Controller层和Service层的完全隔离,但是我开发过程中,VO和DTO基本都是一毛一样,比如“1”代表男性,或者帅哥,我会用两个变量处理,分别为typeCode=1,typeDesc="男性/帅哥",所以很少会存在语义不一致的情况。如果真说会出现不一致的情况,可能我需要将DTO的变量名称转到VO时,会换个变量表达,因为我有时会觉得换一个变量名称表达接口字段的含义,可能会更好一些。不过为了实现这种完全隔离的方式,你让我挨个赋值,我才不干,不过JAVA语言对这个处理还是非常方便,比如通过mapstruct就可以直接搞定,避免你再重复去赋值。但是如果换成Go,嗯。。。我好像没有这个耐心。那换成PHP呢?好像不存在这些东西。
VO与DTO的应用
上面只是用了一个简单的例子来说明VO与DTO在概念上的区别,本节将会告诉你如何在应用中做出正确的选择。
在以下场景中,我们可以考虑把VO与DTO二合为一(注意:是实现层面):
- 当需求非常清晰稳定,而且客户端很明确只有一个的时候,没有必要把VO和DTO区分开来,这时候VO可以退隐,用一个DTO即可,为什么是VO退隐而不是DTO?回到设计层面,服务层的职责依然不应该与展示层耦合,所以,对于前面的例子,你很容易理解,DTO对于“性别”来说,依然不能用“帅哥美女”,这个转换应该依赖于页面的脚本(如JavaScript)或其他机制(JSTL、EL、CSS)。
- 即使客户端可以进行定制,或者存在多个不同的客户端,如果客户端能够用某种技术(脚本或其他机制)实现转换,同样可以让VO退隐。
以下场景需要优先考虑VO、DTO并存:
- 上述场景的反面场景。
- 因为某种技术原因,比如某个框架(如Flex)提供自动把POJO转换为UI中某些Field时,可以考虑在实现层面定义出VO,这个权衡完全取决于使用框架的自动转换能力带来的开发和维护效率提升与设计多一个VO所多做的事情带来的开发和维护效率的下降之间的比对。
- 如果页面出现一个“大视图”,而组成这个大视图的所有数据需要调用多个服务,返回多个DTO来组装(当然,这同样可以通过服务层提供一次性返回一个大视图的DTO来取代,但在服务层提供一个这样的方法是否合适,需要在设计层面进行权衡)。
DTO与DO的区别
首先是概念上的区别,DTO是展示层和服务层之间的数据传输对象(可以认为是两者之间的协议),而DO是对现实世界各种业务角色的抽象,这就引出了两者在数据上的区别。例如UserInfo和User,对于一个getUser方法来说,本质上它永远不应该返回用户的密码,因此UserInfo至少比User少一个password的数据。而在领域驱动设计中,DO不是简单的POJO,它具有领域业务逻辑。
DTO与DO的应用
为什么不在服务层中直接返回DO呢?这样可以省去DTO的编码和转换工作,原因如下:
- 两者在本质上的区别可能导致彼此并不一一对应,一个DTO可能对应多个DO,反之亦然,甚至两者存在多对多的关系。
- DO具有一些不应该让展示层知道的数据
- DO具有业务方法,如果直接把DO传递给展示层,展示层的代码就可以绕过服务层直接调用它不应该访问的操作,对于基于AOP拦截服务层来进行访问控制的机制来说,这问题尤为突出,而在展示层调用DO的业务方法也会因为事务的问题,让事务难以控制。
- 从设计层面来说,展示层依赖于服务层,服务层依赖于领域层,如果把DO暴露出去,就会导致展示层直接依赖于领域层,这虽然依然是单向依赖,但这种跨层依赖会导致不必要的耦合。
DTO应该是一个“扁平的二维对象”,举个例子来说明:如果User会关联若干个其他实体(例如Address、Account、Region等),那么getUser()返回的UserInfo,是否就需要把其关联的对象的DTO都一并返回呢?如果这样的话,必然导致数据传输量的大增,对于分布式应用来说,由于涉及数据在网络上的传输、序列化和反序列化,这种设计更不可接受。如果getUser除了要返回User的基本信息外,还需要返回一个AccountId、AccountName、RegionId、RegionName,那么,请把这些属性定义到UserInfo中,把一个“立体”的对象树“压扁”成一个“扁平的二维对象”。
这里说的非常好,DTO和DO交互时,或者DTO直接和PO交互时,DO和PO中非常多的字段,DTO是不使用的,不应该全部带过来,只需要把DO和PO中需要的字段Copy到DTO中,然后再进行转换。有的同学为了图方便,DTO是直接继承PO或者DO(我之前就这个干过),然后DTO又和PO的字段是一致的,导致通过PO吐出的字段超级多,数据传输大不说,关键是接口输出时,一大坨字段,抓不住重点。
DO与PO的区别
DO和PO在绝大部分情况下是一一对应的,PO是只含有get/set方法的POJO,但某些场景还是能反映出两者在概念上存在本质的区别:
- DO在某些场景下不需要进行显式的持久化,例如利用策略模式设计的商品折扣策略,会衍生出折扣策略的接口和不同折扣策略实现类,这些折扣策略实现类可以算是DO,但它们只驻留在静态内存,不需要持久化到持久层,因此,这类DO是不存在对应的PO的。
- 同样的道理,某些场景下,PO也没有对应的DO,例如老师Teacher和学生Student存在多对多的关系,在关系数据库中,这种关系需要表现为一个中间表,也就对应有一个TeacherAndStudentPO的PO,但这个PO在业务领域没有任何现实的意义,它完全不能与任何DO对应上。
- 某些情况下,为了某种持久化策略或者性能的考虑,一个PO可能对应多个DO,反之亦然。
举两个例子:客户Customer有其联系信息Contacts,这里是两个一对一关系的DO,但可能出于性能的考虑(极端情况,权作举例),为了减少数据库的连接查询操作,把Customer和Contacts两个DO数据合并到一张数据表中。反过来,“角色”与“资源”之间存在多对多关系,会存放到多张表中,而这种关系很明显会表现为一个DO——“权限”。
- PO的某些属性值对于DO没有任何意义,这些属性值可能是为了解决某些持久化策略而存在的数据,例如为了实现“乐观锁”,PO存在一个version的属性,这个version对于DO来说是没有任何业务意义的,它不应该在DO中存在。同理,DO中也可能存在不需要持久化的属性。
总结:DO和PO是两个层级的概念,两者存在一对一、一对多和多对一的关系。这两个层级的字段,DO不需要包括全部的PO,比如PO中的创建、删除标记、自动生成的ID等,PO可以不用关注。
后记
到这里,大家应该对VO、DTO、DO、PO有了比较清晰的认识。其实我对这些O的认识,是从前两个星期才开始的,文章中的大部分内容也是参考网上的经典博客,然后再结合我正在做的项目得到的一些体会,进行的一个简单的总结。
由于我做的项目采用MVC三次架构,所以里面没有DO的概念,但是之前也看过DDD相关的文章,以及公司内部的一些分享,所以也知道个大概,后面打算借助公司的DDD项目,学习DO与DTO和PO的交互实战,以及资源库等相关的实战经验。