引导语
大家好,我是文贺,一名工作五年的一线 Java 开发,主要擅长 Java 源码、DDD(领域驱动设计)、业务中台框架的落地,本次我们分享的主题名称叫做:代码分层后的新世界,其核心思想就是和大家一起讨论下目前比较通用的分层架构,讨论一下其由来、优点和简单的代码落地实践。
2 主要内容
2.1 代码不分层,迟早要崩溃。
14 年参加工作的时候,写了一年的 MVC 架构,大概的后端工程结构(我们说的是后端工程结构)如下:
整个依赖关系是:Controller -> Service -> Model,调用关系也是如此。
使用这种架构,当初我天天做的事情主要是:
- 复制粘贴 Service;
- 复制粘贴 DAO;
- 复制粘贴转化 DTO、VO、DO。
最痛苦的是每次新来需求时,我都是新建一个 Service 接口和类,然后开始复制粘贴,当时我在想:这样写出的代码真的面向对象么?天天这样写代码有什么意义!!我自己又能得到什么成长!!
15 年的时候,幸运的一次机会,我接触到了 DDD,学习了分层框架,从哪开始之后,我再也没有用过 MVC 框架。
当然我们不是说 MVC 框架不好,只是对业务理解不深,对缺少建模指导方法论的新同学来说,MVC 中的 Service 层的定义太模糊了,Service 里面放了各种乱七八糟的东西,比如有消息队列、分布式缓存这种底层技术框架,比如有流程编排的业务逻辑,比如有计算、校验、创建、落库、发消息等复杂动作,可以说 Service 就是一个大杂烩,只要有需求要写代码,我们统统写到 Service 层中,用乱七八糟的 package 进行分层,到项目上线之后,代码看都不想看,其他同事来看代码时,也是很懵,只有一个感觉,Service 层太乱了!
这其实就是 MVC 框架最大的问题:Service 层无结构化,无工程化。
大家可以仔细思考一下,自己在做的项目有没有这种感觉,你觉得你现在的代码逻辑清晰么?你有没有被写代码的前任坑过?你有没有经历过代码乱到你都不敢动的沮丧?
如果你有这种感觉的话,那么恭喜你,你就是本篇文章的最适合阅读人群。
2.2 我应该分层么?
接下来大家在内心中来问自己几个问题:
- 你想不想写出面向对象的代码?
- 在未来 1 年内,你是否还会继续呆在公司?来维护这套代码
- 公司的业务是不是在飞速发展中?
- 公司的业务是不是稍微有点点复杂?
如果以上四个维度你都回答的是:我觉得你可能可以尝试一下分层架构。
从你个人成长上来说,在你在以后的开发生涯中,分层架构你肯定会遇到的,提前尝试对你是有好处的。
随着公司业务的发展扩张,合理的分层结构更容易达到业务的高内聚低耦合,工程更易维护,更易扩展。
当你非常熟悉分层技巧时,分层并不会增加你的工作量,当然在你初次尝试时,由于不熟悉分层代码的结构和落地,肯定会有一些障碍的,但当你跨过了障碍,得到的一定是分层之后的新世界。
2.3 比 MVC 更优秀的分层架构。
我们说的分层架构,主要是分成五层,当然网上有四层、六层的,这都没有关系,四、五、六层虽然最终分的层数不同,但核心思想都是一样的,首先我们一起来看一下下面这张图(请你画一分钟从上而下把图看一遍,先只看,不求理解):
- 这张图代表着一个系统的整体工程结构;
- 这种图从上而下,一共分为五层,分别为:Controller 入口层、App 应用层、Domain 领域层、Spi 层、Infrastructure 基础设施层,网上四层架构是少了 Spi 层,六层架构是多了 Api 层;
- 五层之间的依赖关系是:Controller -> App -> Domain;Spi -> Infrastructure;Infrastructure -> Domain,从依赖关系上看,只有 App 层依赖 Domain 层,Domain 从不会去依赖其他层,这样设计目的是为了让 Domain 层比较稳定,图中层和层之间的箭头表示模块之间的依赖关系;
- 五层之间的调用关系是:Controller -> App -> Domain -> Spi -> Infrastructure,调用关系就是从上而下,比较正常。
- 每一层里面白色长方形标识着自己该干的事情,也就是每层的职责所在。
我们先不深入理解,先整体比较一下这种图和之前 MVC 的图的不同之处:
- 很明显的看出,MVC 后端是三层:Controller 层、Model 层、Service 层,而五层架构多了两层。
- MVC 中的 Controller 层对应五层架构中的 Controller 层,Model 层部分对应五层架构中的 infrastructure 层, Service 层对应五层架构中的 App 层、Domain 层和 Spi 层。
虽然我们现在还不是很清楚五层架构每一层是什么含义,但我们已经明显看到 MVC 中的 Service 层被拆分成了三层,这点是整体上来看最大的不同。
2.4 五层架构的介绍
2.4.1 五层架构的基本介绍
五层架构分为五层,在写代码时,就会有五个模块,每一层对应着一个模块,下图演示的是一个 Maven 工程:
这样在写代码之前,我们首先要思考的是:我的代码应该写在那一层?
接着我们就一层一层的来介绍,因为我们只有知道了每层的职责,才会清楚在每一层中应该写什么样的代码。
2.4.2 Controller 层介绍
Controller 层,也有的同学叫做 Web 层,中文翻译为入口层,比 MVC 中的 Controller 层功能更加丰富。
五层架构中的 controller 主要做三件事情:
- 启动项目;
我们现在的项目基本都是使用的 SpringBoot 架构,在项目启动时,一行代码即可搞定,比如:SpringApplication.run(启动类.class),那么这行启动 Spring 容器的代码,我们就写在 Controller 层。
- 协议转化;
- 接口转化。
2、3 我们统一举一个例子,大家应该买过理财吧,比如我在京东金融上去买理财,理财的页面如下:
声明:图片来自京东金融官网,如有侵权,请联系作者,立马删除。
从图片中我们可以看到,京东金融上的理财产品非常多,而且从页面上我们就可以清晰的看到,这些理财产品的创建者并不都是京东金融,而是来自平安保险、建信基金等等保险基金平台,也就是说京东金融把其他保险基金平台的理财产品放到自己的平台上来卖。
要实现这个目的,京东金融就要对接这些基金保险公司,假设京东金融向外统一的接口是 A,接口定义 10 个字段,协议规定是必须使用京东自己封装的 SKD。
比如现在我们需要对接平安保险,平安保险也有自己的一套对接标准,对外的接口是 B,接口定义 9 个字段,协议规定是 Https。
假设现在由京东金融来适配平台保险,那么适配工作主要是两个:
- 协议转化,京东金融需要开发 Https 的功能,来适配平安保险的协议;
- 由于两家公司的接口出入参的标准都不同,所以京东金融需要做接口转化。
举这个例子,其实就是想说明,如果你有协议转化,接口转化的场景,按照分层职责,代码你可以写在 controller 层。
2.4.3 App 层介绍
App 是英文 Application 的缩写,中文叫做:应用层,主要干一件事情:流程编排。
流程编排的意思是:按照一定的业务场景(流程),将各种领域行为(SpringBean)按照一定的顺序事先编排好,由流程引擎客户端统一执行。
按照这个定义,大家想一想,你平时工作中有用到流程编排么?
有!你肯定有用到,我们平时把 SpringBean 之间的调用逻辑在 App 里面写死,也是一种写死的流程编排,比如这样:
当然更好的是,你在页面上拖拽一下 SpringBean,执行业务逻辑时,就能按照你事先编排的业务逻辑进行执行,做到页面可视可配置的地步。
除了最低的编排功能外,流程引擎还能做:事务开启,组件同步/执行执行等等功能。
所以我们小结一下,App 层的职责:
- 流程编排。
- 事务控制。
除了以上两种事情,其余业务逻辑放进 App 层都是耍流氓。
PS:我们在《面试官系统精讲Java源码及大厂真题》中三个小节中都用流程引擎串 ThreadLocal、ConcurrentHashMap、ThreadPollExecutor、Runnable 等源码的知识点,感兴趣的同学可以关注下。
2.4.4 Domain 层介绍
Domain 层我们叫做领域层,是写业务逻辑的模块,也是领域模型落地的模块,Domain 层的职责如下:
- Domain 层只写核心的业务逻辑,非核心的业务逻辑可以放到 Spi 中;
- App 层直接依赖 Domain 层,这个很好理解,因为代码会从 App 层直接调用到 Domain 层;
- Domain 层从不依赖其他层,Domain 的下游模块会反向依赖 Domain 层。
Domain 层和上下游的依赖关系如下图:
这时候有的同学可能会问,如果 Domain 层要查询数据库(数据库是在 Infrastructure 层的),Domain 层不依赖 Infrastructure 层,是如何查询得到呢?
好问题,答案是:在 Domain 层定义一个接口,Infrastructure 层去实现,在 Domain 层直接依赖接口即可把实现注入,代码如下:
这种模块之间反向依赖的方式,在 DDD(领域驱动设计)中叫做依赖倒置。
接着我们就要来描述 Domain 层和 MVC 中的 Service 层的最大不同了,DDD 将 Service 的代码按照职责进行了细化拆分,拆分成了:实体、聚合、领域服务、应用服务、仓储、工厂等等,当然你不了解这些定义没有关系,你只需要知道按照这样拆分后,最直接的效果是:每个同学在写代码时,再也不简单的新建 Serivce 复制粘贴了,而是会想现在新增的代码是实体么?是聚合么?是领域服务么?
这些领域概念背后都对应着各自的写法规范,让你从此抛弃复制粘贴的漩涡,写出面向对象的代码,至于 Domain 层各个概念之间的关系,我们不会细说(因为比较复杂,内容很多),有兴趣的同学可以看看下面这种图,也可以访问我的博客观看:
2.4.6 Infrastructure 介绍
Infrastructure 中文翻译为基础设施层,主要放一些技术框架的,比如说消息队列中间件、分布式缓存框架、流程引擎、规则引擎、Mysql 数据库等等,我们不在 Infrastructure 层写业务代码,只会放一些和技术有关系的技术框架,这一层相对来说比较容易理解。
2.4.5 Spi 层介绍
SPI 层的由来
SPI 是我们在实践平台化的过程中提出来的。
在一些大型的平台化项目中,我们发现基础设施层会承担很多东西,比如说仓储的实现,有的仓储实现的逻辑是非常复杂的,还比如说我们还会在基础设施层里面调用下游的接口,在里面写一些模型适配的逻辑,慢慢的我们就会觉得别扭,别扭的点如下:
1:我们在基础设施层写的逻辑代码算业务么?
2:基础设施层不应该放技术框架类的东西么,如消息队列框架,远程调用框架等等,不是应该给整个系统提供技术支持么,为什么要写一些仓储实现,模型适配的业务逻辑呢?
我们讨论了很久,最后的结论是:写在基础设施层里面的逻辑也算业务,虽然不是我们核心模型的业务逻辑,但其稳定性和准确性依然会对核心模型产生关键影响。
于是我们单独拆出一个模块来:SPI,一开始取得英文全称:Serial Peripheral Interface,我们把放到基础设施层的代码迁移到了 SPI 层。
在一个基金保险平台项目中,我们甚至成立了一个 SPI 小组,大概 4~8 个人的样子,专门负责对接各种三方基金和保险。
于是也就演变成了我们现在的分层架构,SPI 层也会写一些业务,比如说模型适配,仓储实现,基础设施层就比较干净,都是一些技术框架。
SPI 层的定义
对于 SPI 的定义,我喜欢这么描述:Domain 和下游沟通的桥梁。
SPI 层目前主要负责三大功能:
1:实现 Domain 提出的要求,比如说 Domain 层定义的仓储接口,SPI 层来实现;
2:适配下游三方提出的要求,我们和下游进行对接时,并不是所有的字段 Domain 层都需要关心,对于 Domain 不关心的字段,需要 SPI 自己去填充,定制适配下游的要求;
3:沟通串联 Domain 和下游,使两者可以正确稳定的通信。
SPI 做的事情其实还是满多的,既要满足 Domain 层,又要满足下游,同时还保证 Domain 和下游之间通信的准确和稳定。
2.5 分层带来的好处
好处很多,我们简单列举几个:
- 整体系统的模块分层清晰,有形成一定的规范,易于维护;
- 每一层都可以集中注意力干自己该干的事情;
- 实现了 Domain 层的高内聚低耦合;
- 代码更加面向对象,只要你写过一次这样的代码,你绝对会喜欢上的。
2.6 Show Code
也许很多同学已经听上面的内容很困了,甚至都想睡着了,那么请同学们醒一醒,再坚持一下,和我们一起最后来看下分层代码的分层写法。
首先模块分层我们已经清楚了,接着我们分成一层一层的看下去,我们直接在图上面进行文字描述,这样更容易讲解和观看。
2.6.1 Controller
2.6.1 App
2.6.1 Domain
2.6.1 Spi
2.6.1 Infrastructure
以上截图其实只是展示出 DDD 工程的分层技巧,除了这些,我们对各个领域概念都有指定工程的规范,只要你熟悉我们的工程规范,看代码就完全了解我们整体的领域模型,完全可以了解业务(能做到的同学很少,但只要能做到,升职加薪是必然)。
3 总结
到这里,不知道同学们发现没有,其实这篇文章就是从工程分层的角度出发,给大家展示了一下 DDD 领域驱动设计的魅力,DDD 是一种方法论,如果你苦于:
- 拿到需求文档后,不知道如何分析需求,如何建领域模型;
- 不知道如何画领域模型图,UML 图;
- 不知道如何建数据模型(建表);
- 不知道如何验证领域、数据模型的正确性;
- 不知道如何写出高内聚低耦合的业务代码;
- 不知道如何划分业务、系统、模块之间的边界;
- 关于业务的一切思考。
那么你都可以来学习 DDD,学习 DDD 其实不需要太多基础(有导师带着的话),只需要一颗能静下来的心,
欢迎访问我的博客
我们的课程:《面试官系统精讲Java源码及大厂真题》
作者:文贺
链接:http://www.imooc.com/article/294287