应用分层和领域模型规约-编程思维

前言

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

应用分层

这是阿里巴巴 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 的字段;

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

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

小结

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

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

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/yurunmiao/p/15651375.html

Redis 很屌,不懂使用规范就糟蹋了-编程思维

这可能是最中肯的 Redis 使用规范了 码哥,昨天我被公司 Leader 批评了。 我在单身红娘婚恋类型互联网公司工作,在双十一推出下单就送女朋友的活动。 谁曾想,凌晨 12 点之后,用户量暴增,出现了一个技术故障,用户无法下单,当时老大火冒三丈! 经过查找发现 Redis 报 Could not get a re

HeapDump性能社区Full GC异常问题排查实战案例精选合集-编程思维

处理过线上问题的同学基本都遇到过系统突然运行缓慢,CPU 100%,以及 Full GC 次数过多的问题。这些问题最终导致的直观现象就是系统运行缓慢,并且有大量的报警。 本期小编集合了HeapDump性能社区内的4篇Full GC异常问题排查文章,通过几位作者记录的真实案例,提醒自己避免踩坑,顺便复习相关知识点。 1.

SpringBoot Profiles 多环境配置及切换-编程思维

目录前言默认环境配置多环境配置多环境切换小结 前言 大部分情况下,我们开发的产品应用都会根据不同的目的,支持运行在不同的环境(Profile)下,比如: 开发环境(dev) 测试环境(test) 预览环境(pre) 生产环境(prod) 这里的 环境 实际上是一个统称,不同的环境可能代表着 使用的域名、端口、实例数目是

SpringBoot Logback 日志配置-编程思维

目录前言日志格式日志输出日志轮替日志级别日志分组小结 前言 之前使用 SpringBoot 的时候,总是习惯于将日志框架切换为 Log4j2,可能是觉得比较靠谱,也可能年龄大了比较排斥新东西。今天搞新项目的时候,想着每次搞这个迁移有点儿麻烦,就想着看看 Logback 这个 SpringBoot 一直默认集成(推荐)使

手撕汇编。。。-编程思维

汇编系列文章已经更新了三篇,每一篇都是笔者用心总结,希望对你有帮助 手把手教你汇编 Debug 爱了爱了,这篇寄存器讲的有点意思 之前的文章我们主要聊了一些基本的汇编指令,并且通过一个名为 Debug 的调试软件,让我们看到了内存中是如何存储指令和数据的,在学习了这些之后,我们就可以了解汇编程序了。 程序的执行过程 首

回溯——第77题. 组合-编程思维

回溯的模板和递归是非常相似的: 递归: 确定递归函数的返回值和参数 确定递归的终止条件 确定单层递归的逻辑 回溯: 确定回溯函数的返回值和参数 确定回溯的终止条件,同时在终止时将本次回溯记录的值回传 确定单层回溯的逻辑 无论回溯终止与否,将本次回溯添加的记录值删除 在本题中,代码如下: 1 List<

JavaBean内省与BeanInfo-编程思维

Java的BeanInfo在工作中并不怎么用到,我也是在学习spring源码的时候,发现SpringBoot启动时候会设置一个属叫"spring.beaninfo.ignore",网上只能搜索到这个配置的意思是是否跳过java BeanInfo的搜索,没找到其他信息,但是BeanInfo又是什么呢? JavaBean介

URLDNS分析-编程思维

学习了很久的Java基础,也看了很多的Java反序列化分析,现在也来分析学习哈最基础的URLDNS反序列化吧。 Java反序列化基础 为了方便数据的存储,于是乎有了现在的Java序列化于反序列化。序列化就是将Java对象存储到一个文件,反序列化则是读取序列化生产的文件,还原Java对象,常见的基础反序列化。 首先类需要

消息之短信(云梦网)-编程思维

@目录简介依赖引入yml属性配置配置类工具类 简介 短信通道是由中国移动、联通、电信等运营商直接提供的短信发送接口,实现与客户指定号码进行短信批量发送和自定义发送的目的 需要申请云梦网的测试账号 官方地址-新手指引: [SDK接口文档:http://console.sms.monyun.cn:9963/develope

SpringBoot默认的连接池 HikariCP-编程思维

HikariCP 现在已经有很多公司在使用HikariCP了,HikariCP还成为了SpringBoot默认的连接池,伴随着SpringBoot和微服务,HikariCP 必将迎来广泛的普及。 下面陈某带大家从源码角度分析一下HikariCP为什么能够被Spring Boot 青睐,文章目录如下: 目录 零、类图和流

JavaStudy-编程思维

JavaBasic 创建的文件名需与类名一致,即HelloWorld.java,严格要求大小写。 主函数参数可为String args[]与String[] args。 一个源文件只能有一个public类,且仅为文件名相同的类为public属性。 java文件运行是javac将Hello.java文件编译成Hello.