分层领域模型简介

摘自:《阿里巴巴Java开发手册》

一、分层领域模型规约




说明



DO(Data Object)



数据库表结构一一对应,通过DAO层向上传输数据源对象。



DTO(Data Transfer Object)



数据传输对象,Service或Manager向外传输的对象。若作为分布式服务的参数或返回对象,通常要实现序列化接口。



BO(Business Object)



业务对象。由Service层输出的封装业务逻辑的对象。

此对象在实际使用中有不同的理解,有的团队采用领域驱动设计,BO 含有属性和方法(具体可参考领域驱动设计的相关图书);有的团队将 BO 当做 Service 返回给上层的 “专用 DTO” 使用;而有的团队则当做 Service 层内保存中间信息数据的 “DTO” 或者上下文对象来使用(本文采用这种理解)。

   比如 BO 中可以保存中间状态等,这些并不适合放在 DTO 中



AO(Application Object)



        应用对象。在Web层与Service层之间抽象的复用对象模型,很贴近展示层,复用度低。

        有些团队会将前端查询的属性和保存的属性几乎一致的对象封装为 AO,如读取用户属性传给前端,用户在前端编辑了用户属性后传回后端。这种用法将 AO 用作 Param 和 VO 或 Param 和 DTO 的组合。



VO(View Object)



        显示层对象,通常是Web向模板渲染引擎层传输的对象。

        通常控制层将其作为JSON 返回给前端然后前端渲染,或者加载页面模板在后端进行填充。



Query



        数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止使用Map类来传输。

        Param 为查询参数对象,适用于各层,通常用作接受前端参数对象。Param 和 Query 的出现是为了避免使用 Map 作为接收参数的对象。


二、领域模型命名规约

1) 数据对象:xxxDo,xxx即为数据表名。

2) 数据传输对象:xxxDto xxxBo,xxx为业务领域相关的名称。

3) 展示对象:xxxVo,xxx一般为网页名称。

4) POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。

为什么要有分层领域模型?

        有的朋友查询参数喜欢通过 Map 或者 JSONObject 来封装。有些朋友可能会认为这么多模型没有必要,因为通常各层模型的属性基本相同,而且各种类型的分层模型对象转换非常麻烦。

        使用不同的分层领域模型能够让程序更加健壮、更容易拓展,可以降低系统各层的耦合度。

        ​分层模型的优势只有在系统较大时才体现得更加明显​。设想一下如果我们不想定义 DTO 和 VO,直接将 DO 用到数据访问层、服务层、控制层和外部访问接口上。此时该表删除或则修改一个字段,DO 必须同步修改,这种修改将会影响到各层,这并不符合高内聚低耦合的原则。通过定义不同的 DTO 可以控制对不同系统暴露不同的属性,通过属性映射还可以实现具体的字段名称的隐藏。不同业务使用不同的模型,当一个业务发生变更需要修改字段时,不需要考虑对其它业务的影响,如果使用同一个对象则可能因为 “不敢乱改” 而产生很多不优雅的兼容性行为。

        如果我们不愿意定义 Param 对象,使用 Map 来接收前端的参数,获取时如果采用 JSON 反序列化,则可能出现上一节所讲到的反序列化类型丢失问题。如果我们不使用 Query 对象而是 Map 对象来封装 DAO 的参数,设置和获取的 key 很可能因为粗心导致设置和获取时的 key 不一致而出现 BUG。

流程图

查询视图

Java中的分层领域模型_数据库

        前端或者其它服务将 Param 对象作为参数传给控制层或者对外服务接口,然后调用内部的服务类,服务类内部的中间数据和这些数据相关的逻辑可以封装为 BO ,比如根据 BO 多个属性判断是否符合某个条件。

        如果查询数据则封装为 Query 对象作为参数,如果需要查询其它依赖,则可以封装 Param 对象作为参数去查询。DAO 层一般插入和更新的参数对象使用 DO 或 Param, 查询参数一般使用 Query,删除参数一般使用 Param。

 返回视图

Java中的分层领域模型_数据_02

        数据访问层(DAO)通常将数据封装为 DO 对象传给 Service 层,Manager 或 Client 层往往将查询结果封装为 DTO 传给 Service 层。

        通常内部服务层通过 DTO 往外传输数据。Controller 通常将 DTO 组装为前端需要的 VO 或者直接将 DTO 外传 。

        RPC 服务接口将 DTO 直接返回或者重新封装为新的 DTO 返回给外部服务。

        另外即使同一个接口,但是一个对内使用,一个对外暴露,尽量使用不同接口,定义不同的参数和返回值,从而避免因为修改内部或外部的数据结构而导致另外一个受到影响,这也是单一职责原则的要求。单一职责原则:一个类应该有且只有一个改变的理由。

总结

        也有部分团队 RPC 的请求和响应参数都通过 DTO 来承载,通过 XXRequestDTO 和 XXResponseDTO 来表示。

        实践分层领域模型能够提高项目的健壮性、可拓展性和可维护性,降低了系统内部各层的耦合度。

        上面只是给出一种参考,很多团队对部分分层模型的理解会有差异,实际的使用过程中根据自己团队的规模可以适当变通。比如有很多团队项目并不是特别大,为了降低复杂度,只用到了 DTO 、VO 、DO 三种分层领域模型。

提倡在 DTO 中写逻辑,不要在 RPC 返回对象的 DTO 中封装逻辑。

有些团队的个别成员会将根据成员属性作判断的一些函数写到 DTO 中,最奇葩的是该逻辑还主要供内部系统业务层使用。如:

public class xxDTO{
// 各种属性

// 逻辑代码
public static boolean canXXX(){
// 各种判断
}
}

这样造成系统的耦合性非常强。

如果对方用到了这个函数,未来此函数的内部逻辑必须发生变化,未必能及时通知对方升级,容易造成 BUG。

即使耗费了成本找到了使用方,为了你的功能,让别人被迫升级版本重新上线也是非常不专业的事情。

显然这样做不合理。

试想一下今天 A 部门告诉你他们因某个功能被迫修改了某个 RPC 返回值 DTO 的某个方法,你们用到没有?用到升级一下哈…

然后 B 部门的人明天告诉你同样的话,然后 C 部门,然后…

你会不会崩溃?

建议如果需要在内部业务中写对实体相关的逻辑,可以考虑封装到工具类 / 帮助类中。

其他网址

04.分层领域模型使用解读 - 个人文章 -