代码分层的意义在于将程序逻辑进一步解耦,将层级之间的数据流和依赖关系设计为单向链路,使得系统架构更加灵活易扩展。
一、基本介绍
GoFrame
作为一款工程化完备的基础开发框架,有其独特的框架设计理念,这一章节我们来介绍一下她的代码分层设计。对于服务端业务代码的分层设计模式中,我们比较常见的是MVC
设计模式和三层架构设计模式(3-Tier Architecture
)。
二、MVC
设计模式
我们先来回顾一下经典的MVC
设计模式。
图1. MVC设计模式
简要介绍
M
代表模型(Model
),表示业务规则封装。在MVC
的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。
V
代表视图(View
),用户看到并与之交互的界面。比如由HTML
元素组成的网页界面,或者软件的客户端界面。MVC
的好处之一在于它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操纵的方式。
C
代表控制器(Controller
),接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。
这种设计模式比较简单,比较合适于需要服务端渲染页面的业务场景,对于SEO
来说也比较友好。但随着MVVM
开发模式的兴起,以及前端技术的快速发展,特别是一些前端开发框架如Vue
、React
、Angular
之类的项目出现,服务端的MVC
设计模式使用场景变得越来越少。
痛点描述
针对于业务逻辑并不是特别复杂的业务场景项目,MVC
还能游刃有余,但随着业务逻辑变得庞大复杂,MVC
设计模式的项目维护成本上升的问题变得越来越明显。特别是随着互联网项目微服务架构的发展,MVC
设计模式在大部分的互联网项目开发中变得越来越鸡肋。究其原因,主要的几点:
- 视图展示与数据操作方式的进一步剥离,特别是移动端的发展,前端
MVVM
框架的发展,我们大多数场景下已不再需要服务端渲染View
。 MVC
的代码分层设计模式其实粒度较粗:Model
层级的代码既维护着数据,也封装着业务逻辑,随着业务逻辑变得越来越复杂,这一层功能逻辑会变得越来越臃肿不易维护。- 对于团队管理来讲,
Controller
和Model
的职责边界比较模糊,很难保证参差不齐的团队成员能够清晰地认识到Controller
层并不应当封装业务逻辑。对于开发人员写好代码的要求会比较高。
三、3-Tier Architecture
GoFrame
框架推荐的代码分层设计模式为三层架构设计(3-Tire Architecture
)。三层架构设计能够很好地体现出软件设计”高内聚,低耦合”的设计思想。
图2. 三层架构设计模式
传统的三层架构设计如上图,将项目代码划分了三层,每一层有其独自的职责边界。但在大多数的场景中,我们常看到的是以下的分层结构,将数据结构模型再进一步地抽离了出来统一维护。
图3. 常见三层架构设计模型
表示层 - UI
User Interface
位于三层构架的最上层,与用户直接接触,主要是B/S
中的 WEB
页面,也可以是API
接口。表示层的主要功能是实现系统数据的传入与输出,在此过程中不需要借助逻辑判断操作就可以将数据传送到BLL
系统中进行数据处理,处理后会将处理结果反馈到表示层中。换句话说,表示层就是实现用户界面/API
接口功能,将用户的需求传达和反馈,并用BLL
或者是Model
进行调试,保证用户体验。
业务逻辑层 - BLL
Business Logic Layer
的功能是对具体问题进行逻辑判断与执行操作,接收到表现层UI
的用户指令后,会连接数据访问层DAL
,业务逻辑层在三层构架中位于表示层与数据层中间位置,同时也是表示层与数据层的桥梁,实现三层之间的数据连接和指令传达,可以对接收数据进行逻辑处理,实现数据的增删改查等功能,并将处理结果反馈到表示层UI
中,实现软件功能。
数据访问层 - DAL
Data Access Layer
是数据库的主要操控系统,实现数据的增删改查等操作,并将操作结果反馈到业务逻辑层BLL
。在实际运行的过程中,数据访问层没有逻辑判断能力,为了实现代码编写的严谨性,提高代码阅读程度,一般软件开发人员会在该层中实现通用数据能力进行封装(例如通过ORM
组件)来保证数据访问层DAL
数据处理功能。
模型定义层 - Model
模型定义也常用Entity
实体对象来表示,主要用于数据库表的映射对象,在信息系统软件实际开发的过程中,要建立对象实例,将关系数据库表采用对象实体化的方式表现出来,辅助软件开发中对各个系统功能的控制与操作执行。建立实体类库,进而实现各个结构层的参数传输,提高代码的阅读性。从本质上看,实体类库主要服务于表示层、业务逻辑层以及数据访问层,在三层之间进行数据参数传输,强化数据表示的简约性。
需要注意区分的是,这里的Model
和MVC
设计模式中的Model
虽然都是一个名字但是差别巨大,职责完全不同。
三层架构设计与MVC
由于MVC
也是三层结构,因此有的同学也会将MVC
笼统地归纳于三层架构设计中,从字面意义上来讲似乎也没什么问题。不过两者还是存在一定的区别。
图4. 三层架构设计与MVC
可以看到,在三层架构设计中,UI
表示层即相当于MVC
的View
和Controller
层,原本在MVC
中这两层的逻辑应当是比较”轻量”的,因此被合并为一层进行统一管理也可以理解。比较重要的一点是,原本MVC
中的Model
被拆分为了BLL
和DAL
,即将业务逻辑与数据访问进行分离,将原本臃肿的Model
进行了进一步的解耦,有利于项目的更好维护。
软件架构的演变过程,特别是互联网软件架构的演变过程,本质也就是将业务逻辑不断解耦的过程。
四、框架代码分层结构
GoFrame
业务项目基本目录结构如下:
/
├── app
│ ├── api
│ ├── dao
│ ├── model
│ └── service
├── boot
├── config
├── docker
├── document
├── i18n
├── library
├── packed
├── public
├── router
├── template
├── Dockerfile
├── go.mod
└── main.go
目录/文件名称 | 说明 | 描述 |
---|---|---|
app | 业务逻辑层 | 所有的业务逻辑存放目录。 |
- api | 业务接口 | 接收/解析用户输入参数的入口/接口层。 |
- dao | 数据访问 | 数据库的访问操作,仅包含最基础的数据库 |
- model | 结构模型 | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义。 |
- service | 逻辑封装 | 业务逻辑封装管理,特定的业务逻辑实现和封装。 |
boot | 初始化包 | 用于项目初始化参数设置,往往作为main.go 中第一个被import 的包。 |
config | 配置管理 | 所有的配置文件存放目录。 |
docker | 镜像文件 | Docker 镜像相关依赖文件,脚本文件等等。 |
document | 项目文档 | Documentation项目文档,如: 设计文档、帮助文档等等。 |
i18n | I18N国际化 | I18N国际化配置文件目录。 |
library | 公共库包 | 公共的功能封装包,往往不包含业务需求实现。 |
packed | 打包目录 | 将资源文件打包的Go 文件存放在这里,boot 包初始化时会自动调用。 |
public | 静态目录 | 仅有该目录下的文件才能对外提供静态服务访问。 |
router | 路由注册 | 用于路由统一的注册管理。 |
template | 模板文件 | MVC 模板文件存放的目录。 |
Dockerfile | 镜像描述 | 云原生时代用于编译生成Docker镜像的描述文件。 |
go.mod | 依赖管理 | 使用Go Module 包管理的依赖描述文件。 |
main.go | 入口文件 | 程序入口文件。 |
在实践中,根据项目实际情况增删目录。例如,没有i18n
及template
需求的场景,直接删除对应目录即可。
其中,app
目录下的程序源码维护着项目的具体业务实现,因此这里主要介绍的也是app
目录下的代码结构。
图5. 三层架构设计与框架代码分层映射关系
业务接口 - api
api
包的职责类似于三层架构设计中的UI
表示层,负责接收并响应客户端的输入与输出,包括对输入参数的过滤、转换、校验,对输出数据结构的维护,并调用service
实现业务逻辑处理。
逻辑封装 - service
service
包的职责类似于三层架构设计中的BLL
业务逻辑层,负责具体业务逻辑的实现以及封装。
数据访问 - dao
dao
包的职责类似于三层架构中的DAL
数据访问层,数据访问层负责所有的数据访问收口。
结构模型 - model
model
包的职责类似于三层架构中的Model
模型定义层。模型定义代码层中仅包含全局公开的数据结构定义,往往不包含方法定义。
这里需要注意的是,这里的model
不仅负责维护数据实体对象结构定义,也包括所有的输入/输出数据结构定义,被api/dao/service
共同引用。这样做的好处除了可以统一管理公开的数据结构定义,也可以充分对同一业务领域的数据结构进行复用,减少代码冗余。我们推荐使用struct
结构化管理输入/输出参数类型,以保证软件的开发效率以及可维护性,而不是定义一个interface
参数类型,没人知道到底应该传递什么或者返回什么。
结构化的输入/输出参数也利于编译型语言的静态检测,对CI
友好。
五、常见问题解答
1、框架是否支持常见的MVC
开发模式
当然!作为一款模块化设计的基础开发框架,GoFrame
不会局限代码设计模式,并且框架提供了非常强大的模板引擎核心组件,可快速用于MVC
模式中常见的模板渲染开发。相比较MVC
开发模式,在复杂业务场景中,我们更推荐使大家用三层架构设计模式。
2、如何清晰界定和管理service
和dao
的分层职责
这是一个很经典、并且在任何项目开发中无法绕过的通用问题。
常见的问题是,开发者把数据相关的业务逻辑实现封装到了dao
代码层中,而service
代码层只是简单的dao
调用,这么做的话会使得原本负责维护数据的dao
层代码越来越繁重,反而业务逻辑service
层代码显得比较轻。开发者存在困惑,我写的业务逻辑代码到底应该放到dao
还是service
中?
业务逻辑其实绝大部分时候都是对数据的CURD
处理,这样做会使得几乎所有的业务逻辑会逐步沉淀在dao
层中,业务逻辑的改变其实会频繁对dao
层的代码产生修改。例如:数据查询在初期的时候可能只是简单的逻辑,目前代码放到dao
好像也没问题,但是查询需求增加或变化变得复杂之后,那么必定会继续维护修改原有的dao
代码,同时service
代码也可能同时做更新。原本仅限于service
层的业务逻辑代码职责与dao
层代码职责模糊不清、耦合较重,原本只需要修改service
代码的需求变成了同时修改service
+dao
,使得项目中后期的开发维护成本大大增加。
所以,这里是一个建议。dao
层的代码应该尽量保证通用性,并且大部分场景下不需要增加额外方法,只需要使用一些通用的链式操作方法拼凑即可满足。除非特别必要,那么往往需要知会项目技术Leader
,征得同意后才可执行。
3、不同的代码分层下,都是依靠单目录多文件的思想来区分不同的业务模块,如果业务逻辑复杂后是否会不好管理
在当前推荐的代码分层下,可能有两层的代码会随着业务的发展会变得更加复杂,service
和model
。
1)关于service
代码层
对于service
代码层,通过对象化的逻辑封装之后,每块业务逻辑仅绑定和维护到对应的对象上,并在多人协作时通过不同的文件进行区分,不同业务逻辑的文件名称遵从命名规范,很好维护。但是这里需要注意的是,service
中的资源统一通过对象化封装,仅对外暴露是实例化的对象,不要暴露其他资源(例如结构体、常量、变量、方法等定义),否则service
被对外引用时会显得非常繁杂。一个业务系统假如超过50
个业务逻辑模块已经是相当复杂的系统,所以service
层对外暴露的实例化对象往往不会太多。
2)关于model
代码层
model
代码层中包括数据模型结构定义,以及所有的代码层级间共享的的输入/输出数据结构定义。例如:HttpServer→api
, api→service
, service→dao
就是层级间调用,便需要通过model
管理输入/输出数据结构。如果是api/dao/service
内部方法间的数据结构,不需要公开的场景,那么各自层级在代码内部中自行维护即可,不需要统一定义到model
中。
随着业务的发展,代码层级间共享的数据结构会越来越多,model
代码层中的数据结构也便会越来越多。虽然如service
代码层那样不同的业务模块按照文件名进行划分维护,但是在model
内部所有的数据结构都是共享的,因此数据结构的名称也务必需要保证良好的命名规范,否则被对外引用时也会显得比较繁杂。
4、模型层只有一个包名,所有的数据结构、常量都定义在其中,是否”耦合”太高
model
层内部是一些全局公开的数据结构体定义、全局常量定义,这些资源是只读的,没有任何业务逻辑实现,并且被系统全局引用。需要注意以下几点:
- 模型层包含数据模型,大部分的数据模型与持久化层的数据集合一一对应。
- 模型层包含全局常量定义,但不包含全局变量定义,模型层的资源都是只读的。
- 模型层维护的是全局公开共享资源(资源名称首字母大写),私有资源由其他代码层封装维护。例如,私有的业务常量和结构体可由
service
代码层进行管理,私有的业务变量可由service
代码层中的中”业务领域”对象进行封装管理。
model
层代码是所有分层中逻辑最简单的一块,因为它不包含任何的业务逻辑封装实现。model
层代码没有按照”业务领域”进行内聚封装,而是采用了”访问控制”方式进行聚合管理,并通过文件组织形式进行简单的解耦。这样做的目的是以”务实”为主,使得内部资源可以被有效复用以及被全局便捷访问。
Content Menu