从PO, DTO到Domain Driven Design
从PO, DTO到Domain Driven Design
前言
随着各种模式的层出不穷(MVC, MVP, MVVM...), 一批新概念一批跟一批接上来. 日常开发中经常会使用到PO, DO, BO, VO, DTO. 有时候可能用了很久 也没弄清楚到底怎么区分. 下面我们简单梳理一下
PO, DO, BO, AO, VO, DTO, POJO是什么?
- DTO(Data Transfer Object)数据传输对象
- VO(Value Object)值对象
- AO(Application Object)应用对象
- BO(Business Object)业务对象
- PO(Persistent Object)持久对象
- DO (Data Object) = PO & (Domain Object) = BO
- POJO(plain ordinary java object) 简单无规则 java 对象
情况下面示意图
基本上看下来就能清楚大概了.
DTO(Data Transfer Object) 数据传输对象
概念源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体, 减少分布式调用的次数, 提高性能,降低网络复杂.
但是现在用于服务与服务之间的数据传输对象.
VO (View Object) 视图对象
用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来.
通常是Web向模板渲染引擎层传输的对象.
AO(Application Object)应用对象
在 Web 层与 Service 层之间抽象的复用对象模型,极为贴 近展示层,复用度不高.
BO (Business Object) 业务对象
业务层中使用 ,是 业务对象,封装对象、复杂对象, 可能包含多个类.
由Service层输出的封装业务逻辑的对象.
PO (Persistent Object) 持久对象
此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象.
DO 是什么?
-
阿里巴巴的开发手册
DO( Data Object)这个等同于上面的PO
-
DDD[(Domain-Driven Design)领域驱动设计](#Domain Driven Design 领域驱动设计)
DO(Domain Object)这个等同于上面的BO
POJO(plain ordinary java object) 简单无规则 java 对象
纯的传统意义的 java 对象.重在简单.
有一些private的参数作为对象的属性,针对每一个参数定义get和set方法访问的接口.
没有从任何类继承、也没有实现任何接口,更没有被其它框架侵入的java对象.
Domain Driven Design 领域驱动设计
上文中 DO 提到 DDD (Domain Driven Design)领域驱动设计
架构分成了Interfaces、Applications和Domain三层以及包含各类基础设施的Infrastructure.
架构概述
官方的sample工程,名为DDDSample.
该工程给出了一种实践领域驱动设计的参考架构,本文将对此该架构进行简单介绍,并就一些重要问题进行讨论.
架构分成了Interfaces、Applications和Domain三层关系如下:
详细架构图:
作为参照,下图展示了传统TransactionScript风格的架构:
Transaction Script风格的架构具有明显的“数据”与“操作”分离的特征,其和领域驱动设计风格的架构在两个类组件上有质的区别,一个是领域对象,一个是Service.
领域驱动设计的架构核心目标是要创建一个富领域模型,其典型特征是它的领域对象具有丰富的业务方法用以处理业务逻辑,而Transaction Script风格的领域对象则仅仅是数据的载体,没有业务方法.
在Service方面,领域驱动设计的架构里Service是非常“薄“的一层,其并不负责处理业务逻辑,而在Transaction Script风格的架构里,Service是处理业务逻辑的主要场所,因而往往非常厚重.
架构详解
一. Interfaces 接口层
主要包含与其他系统进行交互的接口与通信设施,在多数应用里,该层可能提供包括Web Services、RMI或Rest等在内的一种或多种通信接口.该层主要由Façade、DTO和Assembler三类组件构成,三类组件均是典型的J2EE模式.
1. DTO
[DTO(Data Transfer Object) 数据传输对象](DTO(Data Transfer Object) 数据传输对象)
2. Assembler
在引入DTO后,DTO与领域对象之间的相互转换工作多由Assembler承担
也有用反射机制自动实现DTO与领域对象之间的相互转换,Apache的Commons BeanUtils就提供了类似的功能.
使用Assembler进行对象数据交换更为安全与可控,并且接受编译期检查,但是代码量明显偏多.使用反射机制自动进行象数据交换虽然代码量很少,但却是非常脆弱的,一旦对象属性名发生了变化,数据交互就会失败,并且很难追踪发现.
3. Facade
作为一种设计模式同时也是Interfaces层内的一类组件,Facade的用意在于为远程客户端提供粗粒度的调用接口.
Facade本身不处理任何的业务逻辑,它的主要工作就是将一个用户请求委派给一个或多个Service进行处理,同时借助Assembler将Service传入或传出的领域对象转化为DTO进行传输.
二. Application应用层
Application层中主要组件是Service,在领域驱动设计的架构里,Service的组织粒度和接口设计依据与传统Transaction Script风格的Service是一致的,但是两者的实现却有着质的区别.
Transaction Script风格的Service是实现业务逻辑的主要场所,因此往往非常厚重.而在领域驱动设计的架构里,Application是非常“薄”的一层,所有的Service只负责协调并委派业务逻辑给领域对象进行处理,其本身并真正实现业务逻辑,绝大部分的业务逻辑都由领域对象承载和实现了,这是区别系统是Transaction Script架构还是Domain Model架构的重要标志.
Service的接口是面向用例设计的,是控制事务、安全的适宜场所.如果Facade的某一方法需要调用两个以上的Service方法,需要注意事务问题.
Domain领域层
Domain层是整个系统的核心层,该层维护一个使用面向对象技术实现的领域模型,几乎全部的业务逻辑会在该层实现.Domain层包含Entity(实体)、Value Object(值对象)、Domain Event(领域事件)和Repository(仓储)等多种重要的领域组件.
三. Infrastructure 基础设施层
作为基础设施层,Infrastructure为Interfaces、Application和Domain三层提供支撑.所有与具体平台、框架相关的实现会在Infrastructure中提供,避免三层特别是Domain层掺杂进这些实现,从而“污染”领域模型.Infrastructure中最常见的一类设施是对象持久化的具体实现.
看到这里可能还云里雾里, 下面我们看一张图
从这张图我们可以看出, 打薄了service, 强调了domain, 把逻辑都放入了domain中实现.
聚合根(Aggreate Root)
聚合根(Aggreate Root, AR)就是软件模型中那些最重要的以名词形式存在的领域对象,比如本文示例项目中的order和product.
然而,并不是说领域模型中的所有名词都可以建模为聚合根.所谓“聚合”,顾名思义,即需要将领域中高度内聚的概念放到一起组成一个整体.
对内聚性的追求会自然地延伸出聚合根的边界.在DDD的战略设计中,我们已经通过限界上下文的划分将一个大的软件系统拆分为了不同的“模块”,在这样的前提下,再在某个限界上下文中来讨论内聚性将比在大泥球系统中讨论变得简单得多.
对聚合根的设计需要提防上帝对象(God Object),也即用一个大而全的领域对象来实现所有的业务功能.上帝对象的背后存在着一种表面上看似合理的逻辑:既然要内聚,那么让我们把所有相关的东西都聚到一起吧,比如用一个Product
类来应付所有的业务场景,包括订单、物流、发票等等.这种机械的方式看似内聚,实则恰恰是内聚性的反面.
- 聚合根的实现应该与框架无关
- 聚合根之间的引用通过ID完成
- 聚合根内部的所有变更都必须通过聚合根完成
- 如果一个事务需要更新多个聚合根,首先思考一下自己的聚合根边界处理是否出了问题,因为在设计合理的情况下通常不会出现一个事务更新多个聚合根的场景.如果这种情况的确是业务所需,那么考虑引入消息机制和事件驱动架构,保证一个事务只更新一个聚合根,然后通过消息机制异步更新其他聚合根.
- 聚合根不应该引用基础设施.
- 外界不应该持有聚合根内部的数据结构.
- 尽量使用小聚合.
资源库(Repository)
用来持久化聚合根的.从技术上讲,Repository和DAO所扮演的角色相似,不过DAO的设计初衷只是对数据库的一层很薄的封装,而Repository是更偏向于领域模型.
应用服务作为总体协调者,先通过资源库获取到聚合根,然后调用聚合根中的业务方法,最后再次调用资源库保存聚合根.
Command 对象
DDD中的写操作并不需要向客户端返回数据,在某些情况下(比如新建聚合根)可以返回一个聚合根的ID,这意味着ApplicationService或者聚合根中的写操作方法通常返回void即可.
从技术上讲,Command对象只是一种类型的DTO对象,它封装了客户端发过来的请求数据.在Controller中所接收的所有写操作都需要通过Command进行包装
DDD中的读操作
领域模型的读操作
这种方式将读模型和写模型糅合到一起,先通过资源库获取到领域模型,然后将其转换为我们需要的VO.
这样导致的结果是Repository上处理了太多的查询逻辑,变得越来越复杂,也逐渐偏离了Repository本应该承担的职责.不推荐使用.
数据模型的读操作
这种方式绕开了资源库和聚合,直接从数据库中读取客户端所需要的数据,此时写操作和读操作共享的只是数据库.
微软也提倡过这种方式,更多细节请参考微软官网.
CQRS (Command Query Responsibility Segregation)
即命令查询职责分离,这里的命令可以理解为写操作,而查询可以理解为读操作.即是逻辑上的读写分离.
传统做法是通过DB 识别sql 的读写操作, 写操作通过主数据库处理事务的增删改,让从数据库处理查询操作(Select操作),数据库复制被用来将事务性操作导致的变更同步到集群中的从数据库.
总结
- 首先我们了解了 [PO, DO, BO, AO, VO, DTO, POJO](#PO, DO, BO, AO, VO, DTO, POJO是什么?)
- 讨论DDD设计模式对于读操作,同样给出了3种方式:
- 基于领域模型的读操作(读写操作糅合在了一起,不推荐)
- 基于数据模型的读操作(绕过聚合根和资源库,直接返回数据,推荐)
- CQRS(读写操作分别使用不同的数据库)
欢迎评论 交流 点赞