前言最近加入新团队,总体框架方向是搭建业务中台,划分子领域、上下文、需求结构和能力可配置性,在领域驱动下,将业务中心整体划分领域,进而划分业务中心具体的能力中心。在这篇文章的开头,我会结合自己的实践经验来谈谈DDD(领域驱动设计)的应用。这里主要说一下我们经常用到的以下几个领域模型:VO、DTO、DO、PO。领域模型中的实体类领域模型中的实体类分为四种模型:VO、DTO、DO和PO。各种实体类用于不同业务层级之间的交互,将实现层级内实体类之间的交互。转换。业务层为:视图层(VIEW+ACTION)、服务层(SERVICE)、持久层(DAO),对应层之间的实体传递如下:VO(ViewObject)视图对象用于表现层,其功能是封装指定页面(或组件)的所有数据。DTO(DataTransferObject)数据传输对象的概念来源于J2EE的设计模式。最初的目的是为EJB分布式应用提供粗粒度的数据实体,从而减少分布式调用的次数,提高分布式调用的性能。并减少网络负载,但在这里,它主要用于表示层和服务层之间的数据传输对象。比如一张表有100个字段,那么对应的DTO就有100个属性(大多数情况下,DTO中的数据来自多个表)。但是视图层只需要显示10个字段,不需要将整个PO对象传递给客户端。这时候我们就可以使用只有这10个属性的DTO向客户端传输数据,这样就不会暴露服务端的表结构。到达客户端后,如果用这个对象来显示相应的界面,那么此时它的身份就会变成VO。DO(DomainObject)领域对象是从现实世界中抽象出来的有形或无形的业务实体。PO(PersistentObject):持久对象与持久层(通常是关系型数据库)的数据结构形成一对一的映射关系。如果持久层是关系型数据库,那么数据表中的每个字段对应PO的一个属性。对于以上概念的理解,未必能形成抽象的思维。我们用时序图搭建模型来描述以上对象在三层架构应用中的位置:用户提交一个请求(可能是填写一个表单),表单上的数据被匹配为VO中的表现层。服务层将VO转换成服务层对应方法需要的DTO发送给服务层。服务层首先根据DTO的数据构造(或重建)DO,调用DO的业务方法完成具体业务。服务层将DO转换为持久层对应的PO(一般使用ORM工具),调用持久层的持久化方法,将PO传递给它,完成持久化操作。对于逆向操作,比如读取数据,也是类似的转换传递。VO和DTOvs.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合二为一(注意:在实现层面):当需求非常明确和稳定,并且只有一个client时,就不需要区分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少一个密码数据。在领域驱动设计中,DO不是简单的POJO,它有领域业务逻辑。DTO和DO的应用会把上面的问题反过来:既然getUser方法返回的UserInfo不应该包含password,那么应该没有password属性定义,但是如果同时有createUser防腐,传入的UserInfo需要包含用户密码,怎么办?在设计层面,从表现层传递给服务层的DTO和服务层返回表现层的DTO在概念上是不同的,但是在实现层面,我们通常很少这样做(定义两个UserInfo,甚至more),因为这样做不是很明智,我们可以设计一个完全兼容的DTO,当服务层接收到数据时,展示层不应该设置的属性(比如订单的trace应该确定通过其单价、数量、折扣等),无论是否设置了展示层,服务层都会忽略它,服务层返回数据时,不应该返回的数据(如用户password)不会设置相应的属性。对于DO,还有一点需要说明:为什么不直接在服务层返回DO呢?这样可以省去DTO的编码和转换工作,原因如下:两者的本质区别不一定是一一对应的,一个DTO可能对应多个DO,反之亦然,甚至还有两者之间是多对多的关系;DO有一些不应该被表现层知道的数据;DO是有业务方法的,如果直接把DO传给表现层,表现层的代码就可以绕过服务层,直接调用不该访问的操作。对于基于AOP拦截服务层的访问控制机制,这个问题尤为突出,在表现层调用DO业务方法也会由于事情的问题而变得难以控制。对于一些ORM框架(如Hibernate),通常会采用“懒加载”技术。如果DO直接暴露给表现层,大多数情况下,表现层不属于事物的范围(Opensessioninview在大多数情况下是不值得设计的),如果它试图获取一个卸载的关联对象时Session关闭,会出现运行时异常(对于Hibernate来说,就是LazyInitliaztionException);从设计层面来说,展示层依赖服务层,服务层依赖领域层。如果DO被暴露,表示层将直接依赖领域层。虽然这仍然是一种单向依赖,但是这种跨层依赖会导致不必要的耦合。对于DTO,还有一点必须说明,那就是DTO应该是一个“平面二维对象”。例如:如果User会关联其他几个实体(如Address、Account、Region等),那么getUser()返回的是UserInfo,是否需要返回其关联对象的所有DTO?如果真是这样,势必会导致数据传输量的大幅增加。对于分布式应用,由于数据在网络上传输,序列化和反序列化,这种设计就更不能接受了。如果getUser除了返回User的基本信息外,还需要返回一个AccountId、AccountName、RegionId、RegionName,那么请在UserInfo中定义这些属性,把一个“三维”的对象树“扁平化”成一个“平面”的二维对象”。DO和POvs.DO和PO的区别大多数情况下,DO和PO是一一对应的。PO是只有get/set方法的POJO,但是有些场景还是可以体现概念上的存在两者的区别:DO在某些场景下不需要显式持久化,比如使用策略模式设计的商品折扣策略,会派生出折扣策略的接口和不同折扣策略的实现类,这些折扣策略实现classes可以看作是DO。但是它们只会驻留在静态内存池中,不需要持久化到持久层。因此,这种DO没有对应的PO。同理,我在某些场景下,PO没有对应的DO。例如,Teacher和Student之间存在多对多关系。在关系型数据库中,这种关系需要表示为一个中间表,对应一个TeacherAndStudentPOPO,但是这个PO在业务领域没有实际意义,根本不能对应任何DO。这里特别说明一下,并不是所有的多对多关系都没有业务意义,这跟具体的业务场景有关,比如:两个PO之间的关系会影响到具体的业务,这种关系有很多种,那么这种多对多关系也应该表示为DO,再比如:“角色”和“资源”之间存在多对多关系,而这种关系显然会表示为DO-“权限””。在某些情况下,出于某种持久化策略或性能考虑,一个PO可能对应多个DO,反之亦然。比如客户Customer有它的联系方式Contacts,这里是两个一对一关系的DO,但是可能是出于性能的考虑(极端情况,举个例子),为了减少connection的查询操作数据库中,将Customer和Contacts两个DO数据合并到一张数据表中。反之,如果一个Book有一个属性是封面封面,但是这个属性是一张图片的二进制数据,有些查询操作不想把封面一起加载,这样可以减少磁盘IO开销,假设ORM框架如果不支持属性级别的懒加载,那么就需要考虑把cover分离成一张数据表,让一个DO对应多个PO。PO的一些属性值对DO没有意义。这些属性值可能是为了解决某些持久化策略而存在的数据。比如为了实现“乐观锁”,PO有一个version属性,这个属性对于DO来说是没有商业意义的,DO中不应该存在的。同样,DO中也可能存在不需要持久化的属性。由于ORM框架的强大功能,DO和PO的应用非常流行,JavaEE也引入了JPA规范。现在的业务应用开发基本上不需要区分DO和PO。PO可以隐藏在JPA、HibernateAnnotations/hbmDO之中。尽管如此,我们还是要注意一些问题:对于DO中不需要持久化的属性,需要通过ORM显式声明,如:在JPA中,可以使用@Transient声明。对于PO中存在的某种持久化策略的属性,比如version,由于DO和PO合并,所以必须在DO中声明,但是由于该属性对于DO没有业务意义,所以需要在DO中隐藏该属性在外部,最常见的方式是将属性的get/set方法私有化,甚至不提供get/set方法。但是对于Hibernate来说,这一点需要特别注意,因为Hibernate在从数据库中读取数据并转化为DO时,是使用反射机制先调用DO的空参构造函数来构造DO实例,然后使用JavaBean规范来实现反映设置方法。为每个属性设置一个值,如果没有显式声明set方法,或者将set方法设置为private,Hibernate将无法初始化DO,导致运行时异常。可行的办法是将属性的set方法设置为protected。对于一个DO对应多个PO,或者一个PO对应多个DO,属性级懒加载的场景,Hibernate提供了很好的支持。请参考Hibnate的相关资料。小结至此,VO、DTO、DO、PO的概念、区别和实际应用已经很清楚了。通过上面的详细分析,我们还可以总结出一个原则:分析设计层面和实现层面是完全独立的层面,即使实现层面可以通过一些技术手段将两个完全独立的概念合二为一,在该层面分析和设计,我们仍然(至少在我们的头脑中)需要清楚地区分概念上独立的事物。这个原则对于良好的分析和设计非常重要(工具越先进,我们就越容易麻木)。
