最近闲来无事,正好前不久将以前到处复制粘贴的代码整理成了一套完整的java 项目模板 ,所以就趁此机会梳理一下个人一直以来的分层逻辑,温故而知新,同时希望可以给后来者一些指导方向,免得后来的初学者像我当初一样对着乱七八糟的项目焦头烂额,完全不知道对方每个包是如何定义的,也不知道如何有效快速地新建一个项目。
本文全为笔者个人的经验之谈,在某些地方可能存在大量的主观建议,如有不同意见,欢迎与笔者联系。
MVC
首先接触后端项目绕不开的就是经典的 MVC,何为 MVC?MVC,即 Model-View-Controller,View 是展示给用户看的界面;Model 是后端编写的模型;Controller 则是连接 View 和 Model 的桥梁,接收 View 的请求发送给 Model,同时收到 Model 处理的结果返回给 View。理解了这三个概念则我们很容易得到一个结论,后端首先需要一个 controller 层来定义与前端交互的接口,其次需要一个 model 层来存储数据库模型,另外每个模型同时需要一些增删改查和一些其他的处理操作,这些逻辑则需要放在 service 层里面,这是后端项目的基础逻辑,下面将详细讲述每个分层的内容。
在 controller 层中,我们一般是需要根据REST 风格 定义 api,简要地说就是以 http 方法来区分对资源的操作类型,GET-查询,POST-新增,PUT-修改,DELTE-删除(不过事实上,笔者写代码这么久很少见到严格遵守 REST 风格的接口,一般都是查询使用 GET,其余均使用 POST)。
mvc 中的 model 事实上我认为是一个比较大的概念,包含了 controller 后面的所有内容,所以下面我将对这块儿内容进行详细描述。
首先可以肯定的是我们在项目初期一定是先定义好的数据库中的各个表,所有逻辑都是围绕着这些表展开,如此则引出了我们需要一个层用来存储数据表的模型,笔者习惯称之为 entity,同时也需要一个层用来与数据库交互,用来直接对数据库操作(一般是直接写 sql),笔者习惯称之为 mapper。
当与数据库的操作定义好之后,我们则需要开始写复杂的业务逻辑了,这些逻辑则需要在 service 层中完成,service 层中不会直接操作数据库,而是调用 mapper 层中的方法。
从数据库到业务逻辑的部分已经完成,接下来则是 controller 和 service 的交互,这里一般上是会定义一层 dto(data transfer object),用来传输 service 和 service 以及 controllerr 和 service 中的数据。而 controller 的接口中接收以及返回的数据则一般会定义在 vo(value object)中。
上面描述了如何将 mvc 的分层思想提现在项目开发中,接下来我将分别以 java 和 go 举例,聊一聊实际项目我是如何实现分层的。
java
java 的目录结构基本都体现在这个项目模板 里了,下面我详细解释一下每个包的作用和输入输出,以及一些可能会出现但是我没有使用的结构。
数据模型:
- entity:数据模型文件,与数据库中的每张表一一对应
- vo:api 接口定义中的使用到的数据模型
- req:api 接口接受的入参
- resp:api 接口返回的出参
- R:这是一个统一返回类,一般包含 code,message,data 三个字段
- dto:用来在各个方法之间传输的对象,一般来讲都有 req->dto->entity->dto->resp 的转换过程,有时也会将 req 或者 resp 省略,在 api 接口中直接接收或者返回 dto。(在模板中笔者为了方便将 dto 省略,直接进行 vo 和 entity 的转换,这里实际上有些不合理,因为一般 service 是不能依赖 vo 的)
数据库操作:
- mapper:直接进行数据库的操作,一般也会被叫做 dao(data access objcet)或者 repository
- manager:阿里规范中不推荐在 service 中直接调用 mapper 的方法,这样会导致逻辑重复以及大事务等问题,因此出现了 manager 层对 mapper 层的方法进行整合(如果使用 mybatis-plus 的话,就是在这里使用 LambdaQueryWrapper 进行查询或者其他操作),以及增加一些缓存等中间操作。(不过一般小项目增加这一中间层意义不大,所以笔者也没有在模板中体现)
业务操作:
- controller: 定义外部调用的接口,输入为 req,输出为 resp,均在 vo 层中定义,controller 中只能调用 service 的方法并将结果进行转换或者整合
- service:编写实际业务逻辑的地方,在 service 中定义 interface
- impl:service 中各个 interface 的实现,只能调用 mapper 或者 manager
杂项:
- annotation:自定义的一些注解,个人习惯注解的定义和逻辑都放在这里
- config:自定义的一些配置
- interceptor:拦截器,可以用来定义一些中间件
- constants:项目中使用到的一些静态常量,一般为枚举类型,不过笔者为了方便都是使用 interface 进行定义
- exception:自定义一些异常
- handler:捕获异常后的处理逻辑
- utils:一些工具类
go
因为 go 的 web 开发中没有 spring 这种大一统的开发框架,所以这导致 go 的目录结构更加扑朔迷离,每个 web 框架都有自己的特性,不过总体上还是可以参考 java 的一套逻辑进行开发,毕竟殊途同归,下面则是以hertz 框架为例讲些常用的一些目录结构。
因为 hertz 可以使用 thrift 生成一些基本的代码,所以笔者给一个简单的 thrift 文件作为例子:
namespace go test_service
struct BaseResp {
1: i32 code
2: string msg
}
struct HelloReq {
1: string text
}
struct HelloResp {
1: i32 code
2: string msg
3: HelloResult data
}
struct HelloResult {
1: string result
}
service HelloHandler {
HelloResp Hello(1: HelloReq req) (api.post="/iapi/test/hello")
}
在使用 hz 命令生成代码后我们会得到一个基本的目录结构,接下来我们对其进行补充,完整的目录结构应该如下所述:
- biz:业务层代码,这里会存放与外部交互的接口及相关文件
- handler:相当于 java 的 controller 中的逻辑部分,定义接口,在这里调用 service 的方法,并对结果进行返回
- model:相当于 java 中的 vo,定义接口的入参和出参
- router:会在这里生成 http 路径的路由代码,以及可以在这里定义一些中间件
- conf:存放配置文件以及读取配置文件的代码
- consts:存放静态常量,因为 go 里面没有枚举类型,所以这里都是使用 const 或者 var 来定义全局常量或者变量(虽然有些是变量,但是一般也不可能去修改他)
- dal:data access layer,数据库操作层
- mongo:执行 mongodb 的操作
- mysql:执行 mysql 的操作
- redis:执行 redis 相关操作
- 。。。其他数据库类似
- init.go:执行上述数据库的初始化操作
- service:实际执行逻辑的代码,调用 dal 中的方法进行数据库的操作
- utils:工具类函数
- main.go:程序的入口,执行一些初始化的操作
- 。。。一些其他生成或者必要的代码文件
DDD
说到项目分层就不得不提一嘴 DDD(Domain-Driven Design),事实上我一直搞不懂也不敢妄言了解 DDD,看过大量关于 DDD 的博客、视频和实际项目,我的总结是:“每个人都有属于自己的 DDD”。犹记得之前看过一个腾讯的论坛,前面几个大佬讲解时评论区一片叫好,而到最后一个大佬讲解 DDD 时,“这个人技术水平有点低”、“他根本不懂 DDD”等类似的言论充斥在评论区。既然有着几年架构经验的腾讯大佬都被评价为“根本不懂”,那我这一个无名小卒大概也只能是说一下自己的浅薄认知了,如有理解不对的地方还望有大佬不吝赐教。(叠甲完毕)
个人认为 DDD 是为了应对传统 MVC 架构中一个服务承担了太多逻辑而出现的,当一个服务承担了多张表相关的业务逻辑时,代码将会膨胀到很难维护的地步,而为了解决此问题,DDD 中将几个相关较大的表(也就是一个域)相关的业务逻辑拆出来独立成一个目录,这样在修改时只需要关注对应域的代码即可,大大降低了维护成本。而相同的逻辑,如统一网关,通用的工具和配置类等也分别定义在一个域中,这样也保证了代码的可复用性。简单来说就是提取出可复用的,将原本的一个大 MVC 拆解成多个小 MVC。(实际上,我并不理解这和微服务有什么区别?)
DDD 中还有个充血模型的概念,但笔者认为这并不是一个好的思路,一个数据模型的类应当保证他的纯洁性,他最多应该只能有字段的定义、构造器、getter、setter 方法,以及一些类似于 from、to、of 等类型转换方法,不应该过分涉及业务逻辑,尤其是对数据库的操作逻辑。
至于 DDD 的分层,这就完全按照每个人自己理解的 DDD 进行分层了,至少到目前为止,笔者还没见到哪两位不同的作者写出来的所谓的 DDD 项目有着相似的分层结构。笔者虽然也接触过一些或许是 DDD 的项目(“或许是”是因为笔者问了身边一些大佬,有的人说这是 DDD,而有的人说不是),但并不认为他们的结构具备参考价值,也就不在这献丑了。