应用分层和领域模型规约

发布于 2022年 01月 25日 12:49

前言

本文讲述的应用分层和领域模型,是我自己根据业务实践过程的一些思考,以及结合目前业界主流的业务规范和技术框架,综合形成的一份实践规约(说明文档)。规约不是标准,主要用于指导自己日后的项目研发,欢迎大家参考讨论。

应用分层

这是阿里巴巴 Java开发手册(嵩山版) 第 6 章节 【工程结构】中推荐的分层结构,如下图:

分层解释说明,请参考手册原文,这里仅讲述我自己的理解。

  • 终端显示层 和 开放API层 可以简单理解为客户端,主要用于发起 HTTP 或 RPC 请求。

  • Web层,也就是 Controller层,主要用于请求参数校验、调用 Service层 处理业务逻辑和返回结果。

  • Service层,主要用于封装实现业务逻辑,这里重点说明一下 Manager层。
    Manager层,主要用于封装 Service层 中的 通用 业务逻辑,实现业务逻辑的复用。注意,业务逻辑也是可复用的组件之一。

    通用的业务逻辑可能有哪些?举几个常用的场景:

    1. 缓存;
    2. Dao 的组合复用;
    3. 其他,Service 层中多次出现的 套路 代码都可以考虑(不是必须)迁移至 Manager 层;
  • Dao层,主要用于封装数据访问逻辑,不只局限于数据库,也可以是数据接口或其他第三方服务;

简化一下分层结构图:

左边的箭头表示数据流入方向:客户端 -> Controller 层 -> Service(Manager) 层 -> Dao 层;
右边的箭头表示数据流出方向:Dao层 -> Service(Manager)层 -> Controller 层 -> 客户端;

数据在每一层之间的流动(流入和流出),它的逻辑业务含义和物理数据结构并不是完全一样的,为了清晰地定义数据位于某一层时的状态,就有了 领域模型 的概念。

领域模型

领域模型本质就是 POJO(Plain Old Java Object)。

什么是 POJO?

Plain:简单的、朴素的;
Old:老旧的,我曾经一度很好奇为什么是 old?后来才理解这里引申为最原始的、最开始的;
Java Object:Java 对象;

POJO 就是最简单的、最原始的普通 Java 对象。

什么是不普通的 Java 对象?

现在大部分的技术框架,都会要求 Java 对象继承特定的类或实现特定的接口,或者被要求打上各式各样的注解,这些 Java 对象就可以看作是不普通的。

领域模块由三部分组成:

  • 类名,表示业务含义;
  • 字段,表示数据结构;
  • 方法,表示支持的操作;

阿里巴巴 Java开发手册(嵩山版) 也给出了领域模型的参考:

  • DO(Data Object):此对象与数据库表结构一一对应,通过DAO层向上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
  • BO(Business Object):业务对象,可以由Service层输出的封装业务逻辑的对象。
  • Query:数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止使用Map类来传输。
  • VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。

网络上关于应用有哪些O,以及每一个O的解释 五花八门,没有对错之分,各有各有道理。本文以 数据在应用分层之间的流动 为视角,讲述一下我自己的理解。

QO(Query Object)

查询对象,用于 Controller 层方法接收客户端的请求参数。

以查询对象 MyQuery 对例:

public class MyQo {
  private String param1;
  private String param2;

  ......
}

Controller 层方法中,查询对象的创建有两种形式:

  1. 框架自动创建 MyQo 对象,且完成请求参数和对象字段的映射;
@PostMapping("/post")
public String post(@RequestBody MyQo qo) {
  ......
}
  1. 人工手动创建 MyQo 对象,且逐一完成请求参数和对象字段的映射;
@GetMapping("/get")
public String get(@RequestParam String param1, @RequestParam String param2) {
  MyQo qo = new MyQo();

  qo.setParam1(param1);
  qo.setParam2(param2);

  ......
}

查询对象创建完成之后,即可作为 Service 层方法的参数:

  service.doSomething(qo);
  ......

这一过程,数据由 客户端 流入 Controller 层。

注意:如果请求参数数目较少,如:1个或2个,则可以不创建查询对象,直接使用请求参数即可。

BO(Business Object)

业务对象,用于 Service 层方法内部逻辑处理,以及向上层(Controller 层)输出业务对象。

  1. Service 层方法内部逻辑处理

以 CRUD 中的 Create 为例,假如我们需要创建一个业务对象(BO),Service 层方法大致可以划分为三步:

1.1 查询对象向业务对象的映射;

  MyBo bo = mapper.map(qo);

客户端 将创建业务对象需要的多个字段使用请求参数的形式流入 Controller 层;Controller 层使用查询对象 qo 接收请求参数,并将查询对象 qo 流入 Service 层,查询对象 qo 中包含有创建业务对象所需的多个字段;查询对象中的字段和业务对象中的字段,字段名称和字段数目不一定是一样的(取决于业务场景),因此需要映射(mapper.map)。

:映射工具由不少的开源技术框架,本文不讨论。

1.2 业务对象向数据对象(DO)的映射;

  MyDo do1 = mapper.map(bo);
  MyDo do2 = mapper.map2(bo);

数据对象(Do)对应数据库的一张数据表,详情见后。业务对象和数据对象不一定是一一对应的,通常一个业务对象对应着多个数据对象。

以简历对象为例,通常简历中会包含:

  • 教育经历
  • 工作经历
  • 项目经历

这些经历的数据会分别存储在不同的数据表中,每一张数据表对应一个数据对象。也就是说,简历对象对应着 3 个或更多的数据对象,因此也需要映射(mapper.map 和 mapper.map2 分别表示将一个业务对象映射到不同的数据对象)。

1.3 调用 Dao 层方法保存数据对象;

  dao1.doSomething(do1);
  dao2.doSomething(do2);

Dao 层方法将数据对象持久化保存到对应的数据表内,然后向上层返回保存结果。

  1. Service 层方法向上层(Controller 层)输出业务对象

以 CRUD 中的 Read 为例,假如我们需要读取一个业务对象(BO),Service 层方法大致可以划分为三步:

2.1 调用 Dao 层方法获取数据对象;

  MyDo do1 = dao1.getSomething1(qo);
  MyDo do2 = dao2.getSomething2(qo);

Dao 层方法使用查询对象(或者根据需要,将查询对象映射为适合 Dao 层方法的查询对象)为参数,读取业务对象对应的若干数据对象。

2.1 数据对象向业务对象的映射;

  MyBo bo = mapper.map(do1, do2);
  
  return bo;

将若干数据对象映射为一个业务对象,然后向上层返回这个业务对象。

这一过程,数据由 Controller 层流入 Service 层,再由 Service 层流入 Dao 层;然后,数据反向流出。

DO(Data Object)

数据对象,每一个数据对象都对应着数据库中的一张数据表,用于 Dao 层方法保存数据对象,以及向上层(Service 层)输出数据对象。

  1. Dao 层方法保存数据对象;
  int saveMyDo(Mydo do);
  1. Dao 层方法向上层(Service 层)输出数据对象;
  Mydo getMyDo(int id);

Dao 层数据对象的保存和读取通常由技术框架帮助完成,不同技术框架实现细节不同,本文不讨论相关内容。

VO(View Object)

显示对象,用于 Controller 层方法返回客户端的请求结果。显示对象来源于 Service 层的数据反向流出,有以下3种情况:

  1. 业务对象

Service 层输出 业务对象,通常见于 Read 场景,这时需要将业务对象映射为显示对象:

  MyVo vo = mapper.map(bo);
  
  return vo;

客户端不需要业务对象的全部字段,或者业务对象的字段需要组合/转换之后才能符合客户端的需求,因此需要映射;映射完成之后,即可以返回给客户端。

  1. 操作结果

操作结果可能是ID、成功或失败、0或1等,通过见于 Create/Update/Delete 场景,这时不需要映射,按协议(客户端和服务端的约定)返回特定结果给客户端即可。

  1. 什么都没有

Service 层方法的返回类型为 void,这种情况实际也是有返回结果的,如:有无异常,按协议返回特定结果给客户端即可。

:Dao 层方法向 Service 层流出时也有类似的情况,不再赘述。

这一过程,数据由 Service 层流出至 Controller 层;然后,数据流出至客户端。

DTO(Data Transfer Object)

数据传输对象,我自己理解用于客户端和服务端之间数据交互的 QO 和 VO 就是很典型的 DTO,可参考这篇文章,不再赘述。

有另一种解读,数据传输对象不仅可以用于端与端之间的数据交互,也可用于层与层之间的数据交互,这一点我也是赞同的。以业务对象为例,如果我们仅仅想查询或更新业务对象的部分字段,那么是否需要为这些仅包含部分字段的业务对象创建专门的模型对象,如:Dto。我的观点是不需要,尽可能复用业务对象(注意不是必须)。那么,如何使用业务对象表述部分字段的业务对象?

业务对象的字段要求全部使用对象类型,不使用基本类型。以 int 为例,定义字段时使用 Integer 替代:

public class MyBo {
  private Integer param1;
  private Double param2;
  private String param3;
  private Object param4;
  ......
}

因为业务对象字段的类型全部使用的是对象类型,我们就可以通过检测字段值是否为 null,以检测结果为条件来执行条件操作

  • 查询业务对象的部分字段,那些不需要被读取的字段可以设置为 null,数据返回时忽略这些值为 null 的字段;
  • 更新业务对象的部分字段,那些不需要被更新的字段可以设置为 null,数据更新时忽略这些值为 null 的字段;

这个要求推荐应用至全部的领域模型对象。

这么做的核心目标是保持系统设计的精简,不为解决特定问题引入特定对象,尽最大程度复用已有对象。虽然会带来实现过程具有一定的复杂度,但我认为值得。

小结

【规约】通俗的讲就是 规则的约定,是一种大家(组织/团队)共同约定好,并一起遵守执行的规则合集;它不是一般意义上的标准规则,不同的 大家 可以使用的规则是不一样的,这一点特别注意。

另外,分层结构和领域模型的设计都是为业务服务的,设计的好坏最终还是要取决于业务效果,适合的就是好的。

推荐文章