GoFrame 工程开发设计-工程目录设计
框架的工程设计没有采用复杂的设计思路,而是采用了务实、稳健和成熟的工程设计,以快速解决业务痛点、降低开发维护成本为第一考量。
一、工程目录结构
GoFrame
业务项目基本目录结构如下(以Single Repo
为例):
/
├── api
├── internal
│ ├── cmd
│ ├── consts
│ ├── controller
│ ├── model
│ │ └── entity
│ └── service
│ └── internal
│ ├── dao
│ └── do
├── manifest
├── resource
├── utility
├── go.mod
└── main.go
工程目录采用了通用化的设计,实际项目中可以根据项目需要适当增减模板给定的目录。例如,没有i18n及template需求的场景,直接删除对应目录即可。
目录/文件名称 | 说明 | 描述 |
api | 接口定义 | 对外提供服务的输入/输出数据结构定义。考虑到版本管理需要,往往以api/v1...存在 |
internal | 内部逻辑 | 业务逻辑存放目录。通过Golang internal特性对外部隐藏可见性 |
-cmd | 入口指令 | 命令行管理目录。可以管理维护多个命令行 |
-consts | 常量定义 | 项目所有常量定义 |
-controller | 接口处理 | 接收/解析用户输入参数的入口/接口层 |
-model | 结构模型 | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
-entity | 数据模型 | 数据模型是模型与数据集合的一对一关系,由工具维护,用户不能修改 |
-service | 逻辑封装 | 业务逻辑封装管理,特定的业务逻辑实现和封装 |
-dao | 数据访问 | 数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的CURD方法 |
-do | 领域对象 | 用于dao数据操作中业务模型与实例模型转换,由工具维护,用户不能修改 |
mainfest | 交付清单 | 包含程序编译、部署、运行、配置的文件 |
-config | 配置管理 | 配置文件存放目录 |
-docker | 镜像文件 | Docker镜像相关依赖文件,脚本文件等等 |
-deploy | 部署文件 | 部署相关的文件。默认提供了Kubernetes集群化部署的Yaml模板,通过kustomize管理 |
resource | 静态资源 | 静态资源文件。这些文件往往可以通过资源打包/镜像编译的形式注入到发布文件中 |
go.mod | 依赖管理 | 使用Go Module包管理的依赖描述文件 |
main.go | 入口文件 | 程序入口文件 |
业务接口 - api
业务接口包含两部分:接口定义(api
)+接口实现(controller
)。
api
包的职责类似于三层架构设计中的UI表示层,负责接收并响应客户端的输入与输出,包括对输入参数的过滤、转换、校验,对输出数据结构的维护,并调用 service
实现业务逻辑处理。
逻辑封装 - service
service
包的职责类似于三层架构设计中的BLL
业务逻辑层,负责具体业务逻辑的实现以及封装。
数据访问 - dao
dao
包的职责类似于三层架构中的DAL
数据访问层,数据访问层负责所有的数据访问收口。
结构模型 - model
model
包的职责类似于三层架构中的Model
模型定义层。模型定义代码层中仅包含全局公开的数据结构定义,往往不包含方法定义。
这里需要注意的是,这里的model
不仅负责维护数据实体对象(entity
)结构定义,也包括所有的输入/输出数据结构定义,被api/dao/service
共同引用。这样做的好处除了可以统一管理公开的数据结构定义,也可以充分对同一业务领域的数据结构进行复用,减少代码冗余。
三层架构设计与框架代码分层映射关系
二、请求分层流转
cmd
cmd
层负责引导程序启动,显著的工作是初始化逻辑、注册路由对象、启动server
监听、阻塞运行程序直至server
退出。
api
上层server
服务接收客户端请求,转换为api
中定义的Req
接收对象、执行请求参数到Req
对象属性的类型转换、执行Req
对象中绑定的基础校验并转交Req
请求对象给controller
层。
controller
controller
层负责接收Req
请求对象后做一些业务逻辑校验,随后调用一个或多个service
实现业务逻辑,将执行结构封装为约定的Res
数据结构对象返回。
model
model
层中管理了所有的业务模型,service
资源的Input/Output
输入输出数据结构都由model
层来维护。
service
service
层的业务逻辑需要通过调用dao
来实现数据的操作,调用dao
时需要传递do
数据结构对象,用于传递查询条件、输入数据。dao
执行完毕后通过Entity
数据模型将数据结果返回给service
层。
dao
dao
层通过框架的ORM
抽象层组件与底层真实的数据库交互。
三、常见问题解答
框架是否支持常见的MVC开发模式
当然!
作为一款模块化设计的基础开发框架,GoFrame
不会局限代码设计模式,并且框架提供了非常强大的模板引擎核心组件,可快速用于MVC
模式中常见的模板渲染开发。相比较MVC
开发模式,在复杂业务场景中,我们更推荐使大家用三层架构设计模式。
如何清晰界定和管理service和controller的分层职责
controller
层处理Req/Res
外部接口请求。负责接收、校验请求参数,并调用一个或多个 service
来实现业务逻辑处理,根据返回数据结构组装数据再返回。
service
层处理Input/Output
内部方法调用。负责内部可复用的业务逻辑封装,封装的方法粒度往往比较细。
因此, 禁止从controller
层直接透传Req
对象给service
,也禁止service
直接返回Res
数据结构对象,因为service
服务的主体与controller
完全不同。当您错误地使用service
方法处理特定的Req
对象的时候,该方法也就与对于的外部接口耦合,仅为外部接口服务,难以复用。这种场景下service
替代了controller
的作用,造成了本末倒置。
如何清晰界定和管理service和dao的分层职责
这是一个很经典的问题。
痛点:
- 常见的,开发者把数据相关的业务逻辑实现封装到了
dao
代码层中,而service
代码层只是简单的dao
调用,这么做的话会使得原本负责维护数据的dao
层代码越来越繁重,反而业务逻辑service
层代码显得比较轻。开发者存在困惑,我写的业务逻辑代码到底应该放到dao
还是service
中? - 业务逻辑其实绝大部分时候都是对数据的CURD处理,这样做会使得几乎所有的业务逻辑会逐步沉淀在
dao
层中,业务逻辑的改变其实会频繁对dao
层的代码产生修改。例如:数据查询在初期的时候可能只是简单的逻辑,目前代码放到dao
好像也没问题,但是查询需求增加或变化变得复杂之后,那么必定会继续维护修改原有的dao
代码,同时service
代码也可能同时做更新。原本仅限于service
层的业务逻辑代码职责与dao
层代码职责模糊不清、耦合较重,原本只需要修改service
代码的需求变成了同时修改service+dao
,使得项目中后期的开发维护成本大大增加。
建议:
- 我们的建议。
dao
层的代码应该尽量保证通用性,并且大部分场景下不需要增加额外方法,只需要使用一些通用的链式操作方法拼凑即可满足。业务逻辑、包括看似只是简单的数据操作的逻辑都应当封装到service
中,service
中包含多个业务模块,每个模块独自管理自己的dao
对象,service
与service
之间通过相互调用方法来实现数据通信而不是随意去调用其他service
模块的dao
对象。