工程目录设计是代码分层设计的进一步落地,建议您先仔细阅读: 代码分层设计
这是 GoFrame
框架针对 业务项目 的目录设计,主体的思想来源于三层架构,但在具体实现中,对其进行了一定的改进和细化使其更符合工程实践和时代进步。
一、工程目录结构
GoFrame
业务项目基本目录结构如下(以 Single Repo
为例):
/
├── api
├── hack
├── internal
│ ├── cmd
│ ├── consts
│ ├── controller
│ ├── dao
│ ├── logic
│ ├── model
│ | ├── do
│ │ └── entity
│ └── service
├── manifest
├── resource
├── utility
├── go.mod
└── main.go
🔥 重要提示 🔥:框架的工程目录采用了 通用化的设计,以满足不同复杂程度业务项目的需求,但实际项目中可以根据项目需要 适当增减默认的目录。例如,没有 i18n/template/protobuf
需求的场景, 直接删除对应目录即可。又例如,非常简单的业务项目(如验证/演示项目),不考虑使用严谨的 dao/logic/model
目录及特性,那么 直接删除对应目录即可,可以在 controller
中直接实现业务逻辑。 一切都可以由开发者灵活选择组装!
目录/文件名称 | 说明 | 描述 |
---|---|---|
api | 对外接口 | 对外提供服务的输入/输出数据结构定义。考虑到版本管理需要,往往以 api/xxx/v1... 存在。 |
hack | 工具脚本 | 存放项目开发工具、脚本等内容。例如, CLI 工具的配置,各种 shell/bat 脚本等文件。 |
internal | 内部逻辑 | 业务逻辑存放目录。通过 Golang internal 特性对外部隐藏可见性。 |
- cmd | 入口指令 | 命令行管理目录。可以管理维护多个命令行。 |
- consts | 常量定义 | 项目所有常量定义。 |
- controller | 接口处理 | 接收/解析用户输入参数的入口/接口层。 |
- dao | 数据访问 | 数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的 CURD 方法 |
- logic | 业务封装 | 业务逻辑封装管理,特定的业务逻辑实现和封装。往往是项目中最复杂的部分。 |
- model | 结构模型 | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义。 |
- do | 领域对象 | 用于 dao 数据操作中业务模型与实例模型转换,由工具维护,用户不能修改。 |
- entity | 数据模型 | 数据模型是模型与数据集合的一对一关系,由工具维护,用户不能修改。 |
- service | 业务接口 | 用于业务模块解耦的接口定义层。具体的接口实现在 logic 中进行注入。 |
manifest | 交付清单 | 包含程序编译、部署、运行、配置的文件。常见内容如下: |
- config | 配置管理 | 配置文件存放目录。 |
- docker | 镜像文件 | Docker 镜像相关依赖文件,脚本文件等等。 |
- deploy | 部署文件 | 部署相关的文件。默认提供了 Kubernetes 集群化部署的 Yaml 模板,通过 kustomize 管理。 |
- protobuf | 协议文件 | GRPC 协议时使用的 protobuf 协议定义文件,协议文件编译后生成 go 文件到 api 目录。 |
resource | 静态资源 | 静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中。 |
go.mod | 依赖管理 | 使用 Go Module 包管理的依赖描述文件。 |
main.go | 入口文件 | 程序入口文件。 |
对外接口
对外接口包含两部分:接口定义( api
)+接口实现( controller
)。
服务接口的职责类似于三层架构设计中的 UI
表示层,负责接收并响应客户端的输入与输出,包括对输入参数的过滤、转换、校验,对输出数据结构的维护,并调用 service
实现业务逻辑处理。
接口定义 - api
api
包用于与客户端约定的数据结构输入输出定义,往往与具体的业务场景强绑定。
接口实现 - controller
controller
用于接收 api
的输入,可以直接在 controller
中实现业务逻辑,或者调用一个或多个 service
包实现业务逻辑,将执行结果封装为约定的 api
输出数据结构。
业务实现
业务实现包含两部分:业务接口( service
)+业务封装( logic
)。
业务实现的职责类似于三层架构设计中的 BLL
业务逻辑层,负责具体业务逻辑的实现以及封装。
在后续的章节介绍中,我们会将业务实现统一称作 service
,大家注意它其实包含两部分即可。
业务接口 - service
service
包用于解耦业务模块之间的调用。业务模块之间往往不会直接调用对应的业务模块资源来实现业务逻辑,而是通过调用 service
接口。 service
层只有接口定义,具体的接口实现注入在各个业务模块中。
业务封装 - logic
logic
包负责具体业务逻辑的实现以及封装。项目中各个层级代码不会直接调用 logic
层的业务模块,而是通过 service
接口层来调用。
结构模型
model
包的职责类似于三层架构中的 Model
模型定义层。模型定义代码层中仅包含全局公开的数据结构定义,往往不包含方法定义。
这里需要注意的是,这里的 model
不仅负责维护数据实体对象( entity
)结构定义,也包括所有的输入/输出数据结构定义,被 api/dao/service
共同引用。这样做的好处除了可以统一管理公开的数据结构定义,也可以充分对同一业务领域的数据结构进行复用,减少代码冗余。
数据模型 - entity
与数据集合绑定的程序数据结构定义,通常和数据表一一对应。
业务模型 - model
与业务相关的通用数据结构定义,其中包含大部分的方法输入输出定义。
数据访问 - dao
dao
包的职责类似于三层架构中的 DAL
数据访问层,数据访问层负责所有的数据访问收口。
三层架构设计与框架代码分层映射关系
二、请求分层流转
cmd
cmd
层负责引导程序启动,显著的工作是初始化逻辑、注册路由对象、启动 server
监听、阻塞运行程序直至 server
退出。
api
上层 server
服务接收客户端请求,转换为 api
中定义的 Req
接收对象、执行请求参数到 Req
对象属性的类型转换、执行 Req
对象中绑定的基础校验并转交 Req
请求对象给 controller
层。
controller
controller
层负责接收 Req
请求对象后做一些业务逻辑校验,可以直接在 controller
中实现业务逻辑,或者调用一个或多个 service
实现业务逻辑,将执行结果封装为约定的 Res
数据结构对象返回。
model
model
层中管理了所有的业务模型, service
资源的 Input/Output
输入输出数据结构都由 model
层来维护。
service
service
是接口层,用于解耦业务模块, service
没有具体的业务逻辑实现,具体的业务实现是依靠 logic
层注入的。
logic
logic
层的业务逻辑需要通过调用 dao
来实现数据的操作,调用 dao
时需要传递 do
数据结构对象,用于传递查询条件、输入数据。 dao
执行完毕后通过 Entity
数据模型将数据结果返回给 service
层。
dao
dao
层通过框架的 ORM
抽象层组件与底层真实的数据库交互。
三、常见问题解答
框架是否支持常见的 MVC
开发模式
当然!
作为一款模块化设计的基础开发框架, GoFrame
不会局限代码设计模式,并且框架提供了非常强大的模板引擎核心组件,可快速用于 MVC
模式中常见的模板渲染开发。相比较 MVC
开发模式,在复杂业务场景中,我们更推荐使大家用三层架构设计模式。
当 api
与 model
层存在重复数据结构时如何维护
在 api
中定义的数据结构是 对外使用的,与具体的业务场景绑定(如具体的页面交互逻辑、单一的接口功能),数据结构是由上层展示层前置决定的; model
中定义的数据结构是 服务内部使用的,数据结构是在接口实现和抽象的过程中才能定义的,并且 model
中的数据结构可以随意在内部修改而并不会影响对外 api
接口的兼容性。
注意 model
中的数据结构不应该直接暴露给外部使用,并且在框架的工程设计中刻意将 model
目录放到了 internal
目录下。也不应该在 api
层中对 model
中的数据结构进行 别名类型定义 供外部访问。一旦将 model
中的数据结构应用到了 api
层中,内部 model
数据结构的修改会直接影响到 api
接口的兼容性。
如果两者出现重复的数据结构(甚至常量、枚举类型),建议将数据结构定义到 api
层中。服务内部逻辑可以直接访问 api
层的数据结构。 model
层的数据结构也可以直接引用 api
层的数据结构,但是反之则不行。
我们来看一个示例,便于更好地理解:
如何清晰界定和管理 service
和 controller
的分层职责
controller
层处理 Req/Res
外部接口请求。负责接收、校验请求参数,可以直接在 controller
中实现业务处理逻辑,或者调用 一个或多个 service
来实现业务逻辑处理,将执行结果封装为约定的 api
输出数据结构返回。 service
层处理 Input/Output
内部方法调用。负责内部 可复用 的业务逻辑封装,封装的方法粒度往往比较细。
通常来讲,开发接口时只需要编写 controller
层中的接口实现业务逻辑即可,当存在重复代码逻辑时,再从各个 controller
接口实现逻辑中抽象沉淀到 service
层。如果从 controller
层直接透传 Req
对象给 service
,同时 service
直接返回 Res
数据结构对象,该方法也就与外部接口耦合,仅面向外部接口服务,难以复用,这样会增加技术债务成本。
如何清晰界定和管理 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
对象。
为什么要使用 internal
目录包含业务代码
internal
目录是 Golang
语言专有的特性,防止同级目录外的其他目录引用其下面的内容。业务项目中存在该目录的目的,是避免若项目中存在多个子项目(特别是大仓管理模式时),多个项目之间无限制随意访问,造成难以避免的多项目不同包之间耦合。