领域驱动模型 VO、DTO、DO、PO 概念及其区别

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有个人系列文章。git

前言

最近加入到一个新的团队,总体的框架方向是构建业务中台,划分子域、上下文、需求结构化和能力可配置,是经过领域驱动,从总体上划分业务中台的领域,进而划分出业务中台的具体能力中心,。本篇文章开始,将会结合本身的实际经验,聊一聊DDD(领域驱动设计)的应用。这里咱们主要聊如下咱们常常会用的的领域模型:VO、DTO、DO、PO。github

领域模型中的实体类

领域模型中的实体类分为四种模型:VO、DTO、DO、PO,各类实体类用于不一样业务层次间的交互,并会在层次内实现实体类之间的转化。数据库

业务分层为:视图层(VIEW+ACTION)、服务层(SERVICE)、持久层(DAO),相应各层间实体的传递以下: 设计模式

VO (View Object)视图对象

用于展现层,它的做用是把某个指定页面(或组件)的全部数据封装起来。markdown

DTO(Data Transfer Object)数据传输对象

这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减小分布式调用的次数,从而提升分布式调用的性能和下降网络负载,可是这里,主要用于展现层与服务层之间的数据传输对象。网络

好比一张表有100个字段,那么对应的DTO就有100个属性(大多数状况下,DTO内的数据来自多张表)。可是view层只须要显示10个字段,没有必要把整个PO对象传递到 client,这时咱们就能够用只有这10个属性的DTO来传输数据到 client,这样也不会暴露 server 端的表结构。到达客户端后,若是用这个对象来对应界面展现,那么此时它的身份就转为 VO。session

DO(Domain Object)领域对象

就是从现实世界中抽象出来的有形或无形的业务实体。数据结构

PO(Persistent Object):持久化对象

它跟持久层(一般是关系型数据库)的数据结构造成一一对应的映射关系,若是持久层是关系型数据库,那么,数据表中的每一个字段就对应PO的一个属性。架构

对于以上概念的理解,可能还不能造成一种抽象化思惟,咱们经过一个时序图创建模型来描述上述对象在三层架构应用中的位置: 框架

  • 用户提交请求(多是填写表单),表单的数据在展现层被匹配为 VO。
  • 服务层把 VO 转换为服务层对应方法所要求的 DTO,传送给服务层。
  • 服务层首先根据 DTO 的数据构造一个 DO (或重建),调用 DO 的业务方法完成具体业务。
  • 服务层把 DO 转换为持久层对应的 PO(通常使用 ORM 工具),调用持久层的持久化方法,把 PO 传递给它,完成持久化操做。

对于一个逆向操做,如读取数据,也是用相似的方式转换和传递。

VO 与 DTO 对比

VO 与 DTO 的区别

在这里咱们可能会问:既然 DTO 是展现层与服务层之间传递数据的对象,为何还要一个 VO 呢?

是的,对于绝大部分的应用场景来讲,DTO 和 VO 的属性值基本是一致的,并且他们一般都是 POJO,所以不必画蛇添足。但不要忘记这是实现层的思惟,对于设计层面来讲,概念上仍是应该存在 VO 和 DTO,所以二者有着本质的区别,DTO 表明服务层须要接收的数据和返回的数据,而 VO 表明展现层须要显示的数据。

用一个例子来讲明可能会比较容易理解:

例如:Service 层有一个 getUser 的方法返回一个系统用户,其中有一个属性是 gender(性别),对于 Service 层来讲,它只从语义上定义:1-男性、2-女性、0-未指定,而对于展现层来讲,它可能须要用“帅哥”表明男性、“美女”表明女性、“秘密”表明未指定。

说到这里,可能你还会反驳,在服务层直接返回“帅哥、美女”不就行吗?对于大部分应用来讲,这不是问题,但设想如下,若是需求容许客户能够定制风格,而不一样的客户端对于表现层的要求有所不一样,那么,问题就来了。再者,回到设计层面分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,所以,它返回的 DTO,不该该出现与表现形式的耦合。

理论归理论,这到底仍是分析设计层面的思惟,是否在具体实现层面必须这样作呢?一刀切的作法每每会得不偿失,下面咱们具体分析应用中如何作出正确的选择。

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 的区别

首先是概念上的区别,DTO 是展现层和服务层之间的数据传输对象(能够认为是二者之间的协议),而 DO 是对现实世界各类业务角色的抽象,这就引出了二者在数据上的区别。

例如:UserInfo 和 User ,对于一个 getUser 方法来讲,本质上它永远不该该返回用户的密码,所以 UserInfo 至少比 User 少一个 password 的数据。而在领域驱动设计中,DO不是简单的POJO,它具备领域业务逻辑。

DTO 与 DO 的应用

从上面会反向问题:既然 getUser 方法返回的 UserInfo 不该该包含 password,那么就不该该存在 password 这个属性定义,可是若是同时有一个 createUser 的防腐,传入的UserInfo须要包含用户的 password,怎么办?

在设计层面,展现层向服务层传递的 DTO 与 服务层返回给展现层的 DTO 在概念上是不一样的,但在实现层面,咱们一般不多会这样作(定义两个 UserInfo,甚至更多),由于这样作并不见得很明智,咱们彻底能够设计一个彻底兼容的DTO,在服务层接收数据的时候,不该该由展现层设置的属性(如订单的踪影应该由其单价、数量、折扣等决定),不管展现层是否设置,服务层都一律忽略,而在服务层返回数据时,不应返回的数据(如用户密码),就不设置对应的属性。

对于DO来讲,还有一点须要说明:为何不在服务层中直接返回 DO 呢?这样能够省去 DTO 的编码和转换工做,缘由以下:

  • 二者在本质上的区别可能致使彼此并不一一对应,一个DTO可能对应多个DO,反之亦然,甚至二者存在多对多的关系;
  • DO 具备一些不该该让展现层知道的数据;
  • DO 具备业务方法,若是直接把 DO 传递给展现层,展现层的代码就能够绕过服务层直接调用它不该该访问的操做,对于基于 AOP 拦截服务层来进行访问控制的机制来讲,这问题尤为突出,而在展现层调用DO的业务方法也会由于事物的问题,让事物难以控制。
  • 对于某些ORM框架(如Hibernate)来讲,一般会使用“延迟加载”技术,若是直接把 DO 暴露给展现层,对于大部分状况,展现层不在事物范围以内(Open session in view 在大部分状况下不是一种值得推崇的设计),若是其尝试在Session关闭的状况下获取一个未加载的关联对象,会出现运行时异常(对于Hibernate来讲,就是 LazyInitliaztionException);
  • 从设计层面来讲,展现层依赖于服务层,服务层依赖于领域层,若是把DO暴露出去,就会致使展现层直接依赖于领域层,这虽然依然单向依赖,但这种跨层依赖会致使没必要要的耦合。

对于DTO来讲,也有一点必须进行说明,就是DTO应该是一个“扁平的二维对象”举个例子:

  • 若是User 会关联若干个其余实体(例如 Address、Account、Region等),那么 getUser() 返回的 UserInfo,是否就须要把其关联的对象的 DTO 都一并返回呢?若是这样的话,必然致使数据传输量的大增,对于分布式应用来讲,因为涉及数据在网络上传输、序列化和反序列化,这种设计更不可接受。
  • 若是getUser除了要返回User的基本信息外,还须要返回一个AccountId、AccountName、RegionId、RegionName,那么,请把这些属性定义到UserInfo中,把一个“立体”的对象树“压扁”成一个“扁平的二维对象”。

DO 与 PO 对比

DO 与 PO 的区别

DO 和 PO 在绝大部分状况下是一一对应的,PO是只含有 get/set 方法的POJO,但某些场景仍是能反映出二者在概念上存在本质区别:

  • DO 在某些场景下不须要进行显式的持久化,例如利用策略模式设计的商品折扣策略,会衍生出折扣策略的接口和不一样折扣策略实现类,这些折扣策略实现类能够算是 DO,但它们只会驻留在静态内存池,不须要持久化到持久层,所以,这类 DO 是不存在对应的 PO的。
  • 一样的道理,某些场景下,PO也没有对应的DO,例如老师Teacher和学生Student存在多对多的关系,在关系数据库中,这种关系须要表现为一个中间表,也就对应有一个TeacherAndStudentPO的PO,但这个PO在业务领域没有任何现实的意义,它彻底不能与任何DO对应上。

这里要特别声明,并非全部多对多关系都没有业务含义,这跟具体业务场景有关,例如:两个PO之间的关系会影响具体业务,而且这种关系存在多种类型,那么这种多对多关系也应该表现为一个DO,又如:“角色”与“资源”之间存在多对多关系,而这种关系很明显会表现为一个DO——“权限”。

某些状况下,为了某种持久化策略或者性能的考虑,一个PO可能对应多个DO,反之亦然。例如客户Customer有其联系信息Contacts,这里是两个一对一关系的DO,但可能出于性能的考虑(极端状况,权做举例),为了减小数据库的链接查询操做,把Customer和Contacts两个DO数据合并到一张数据表中。反过来,若是一本图书Book,有一个属性是封面cover,但该属性是一副图片的二进制数据,而某些查询操做不但愿把cover一并加载,从而减轻磁盘IO开销,同时假设ORM框架不支持属性级别的延迟加载,那么就须要考虑把cover独立到一张数据表中去,这样就造成一个DO对应多个PO的状况。 PO的某些属性值对于DO没有任何意义,这些属性值多是为了解决某些持久化策略而存在的数据,例如为了实现“乐观锁”,PO存在一个version的属性,这个version对于DO来讲是没有任何业务意义的,它不该该在DO中存在。同理,DO中也可能存在不须要持久化的属性。

DO 与 PO 的应用

因为ORM框架的功能很是强大而大行其道,并且JavaEE也推出了JPA规范,如今的业务应用开发,基本上不须要区分DO与PO,PO彻底能够经过JPA,Hibernate Annotations/hbm隐藏在DO之中。虽然如此,但有些问题咱们还必须注意:

  • 对于DO中不须要持久化的属性,须要经过ORM显式的声明,如:在JPA中,能够利用@Transient声明。
  • 对于PO中为了某种持久化策略而存在的属性,例如version,因为DO、PO合并了,必须在DO中声明,但因为这个属性对DO是没有任何业务意义的,须要让该属性对外隐藏起来,最多见的作法是把该属性的get/set方法私有化,甚至不提供get/set方法。但对于Hibernate来讲,这须要特别注意,因为Hibernate从数据库读取数据转换为DO时,是利用反射机制先调用DO的空参数构造函数构造DO实例,而后再利用JavaBean的规范反射出set方法来为每一个属性设值,若是不显式声明set方法,或把set方法设置为private,都会致使Hibernate没法初始化DO,从而出现运行时异常,可行的作法是把属性的set方法设置为protected。
  • 对于一个DO对应多个PO,或者一个PO对应多个DO的场景,以及属性级别的延迟加载,Hibernate都提供了很好的支持,请参考Hibnate的相关资料。

总结

到目前为止,已经比较清晰的了解VO、DTO、DO、PO的概念、区别和实际应用了。经过上面的详细分析,咱们还能够总结出一个原则:分析设计层面和实现层面彻底是两个独立的层面,即便实现层面经过某种技术手段能够把两个彻底独立的概念合二为一,在分析设计层面,咱们仍然(至少在头脑中)须要把概念上独立的东西清晰的区分开来,这个原则对于作好分析设计很是重要(工具越先进,每每会让咱们越麻木)。

文章持续更新,能够公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

相关文章
相关标签/搜索