⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Aug.14 2021
本作品由 ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。
1. 服务分层

在开发后端服务的时候,我们通常把它拆解为三部分:Handler, Service 和 DAL,如上图所示。
1.1 Motivation:为什么分层?
在后端的处理逻辑中,涉及到:HTTP Request 路由与解析、业务核心逻辑、数据库访问,RPC 过程等等,如果把这些流程全部揉在一起,将会十分冗杂不好维护,所以进行了分层。
另一个动机就是:进行了分层之后,我们可以很方便地对各个层的功能和性能进行测试,测试的方法就是将想要进行测试的层所依赖的底下一层进行替换,替换为一个 "Mock" 的层,这个 Mock 的层并不会去真的执行复杂的逻辑,它会简单地提供我们预想之内的回复,然后我们从顶层给测试层一个 "激励",由于测试层的依赖层已经设置为我们预想中的情况,所以如果测试层能够给出正确的 Response,或者说能够达到预期的性能,那么我们的测试结果就是积极的;否则证明程序存在 bug。
我们在后面 分层示例 会通过 golang 来展示分层的细节。
1.2 Model
首先,我们必须先将我们在处理的服务中涉及到的数据具象化为 Struct,比如用户、订单等,这就形成了 Model 层。我们在下面会看到,Model 层所定义的 Struct 将在 Handler, Service 和 DAL 之间进行传递。
1.3 Handler
在后端服务开发中,在顶层,我们首先会编写 "端点路由" 的程序,根据 url 和 HTTP Request 的类型 (GET/POST/DELETE...),将 HTTP Request "路由" 到某个 Handler 下,Handler 就是一个处理函数,就像我们在 Golang 中使用的 Gin 框架那样。在设计后端服务时,针对每一个我们设计的 URL API,我们都应该有一个 Handler 去承接它。Handler 作为直面 HTTP Request 的第一层程序,专门用于解析 HTTP Request,包括 Header 和 Body。比如 Handler 可能会对 HTTP Header 中的 "Content-Type" 字段进行验证 (validation),或者还会对 Body 中的 JSON 数据是否合法进行验证,在验证无误后,通常会将 Body (i.e. 通常是 JSON 或者 XML) 中携带的数据转化为程序对象/结构,即我们在上面提到的在 Model 层中定义的 Struct。
当 Handler 层将 HTTP Request "收纳" 和解析完成之后,就会调用相应的 Service 层的程序, Service 层的程序用户负责处理后端业务的核心逻辑。
1.4 Service
当 Handler 处理完 HTTP Request 后,根据请求的内容调用 Service 层的相应程序,并将相应的数据以 Model Struct 的形式传入。Service 在拿到响应数据后,就会执行核心后端逻辑,比如列表排序、总数计算等等。在必要时,还会调用 DAL 层的程序,DAL 层的程序是负责直接与数据库进行交互的程序。
1.5 DAL (Data Access Layer)
为了把和数据库直接进行交互的代码和其它代码分隔开,我们将这部分代码专门放在 DAL 层的程序中。DAL 层的代码被 Service 层的程序调用,注意到 Service 层也是将数据以 Model 层定义的 Struct 的形式传到 DAL 层,DAL 层将相应的数据封装成为数据库查询语言的格式,从数据库中获取数据,然后再把这部分新获取的数据同样封装为 Model 层 Struct 返还给 Service 层程序。
2. 分层示例
我们在 Motivation:为什么分层? 中稍微解释了分层的思路,在本章中我们将结合 Golang 继续探讨。
在分层的思路下,我们可以利用 Golang 提供的 Interface 机制,来实现对每一层所需要实现的函数的约定。比如可以定义 Service 层的接口,约束 Service 层必须实现 GetUserById(...) 和 AddUserProfile(...) 的逻辑。这样一来,我们在 Handler 层中,就只需要使用 Interface 就可以了,并不需要去调用一个具体的 Service 层的实现。在这样的程序下,我们就可以轻松地替换 Service 层的具体实现,从而提供我们在 Motivation:为什么分层? 中提到的 Mock 的 Service 层。
例如,我们可以首先定义 Service 层的接口:
1
2
3
4type UserService interface {
GetUserProfileById(ctx context.Context, uid uuid.UUID) (*UserProfile, error)
Signup(ctx context.Context, uc *UserCertificate, up *UserProfile) error
}
然后,我们会使用某个具体的实现这个接口的结构去初始化 Handler,具体是哪个具体实现并没有确定。在 Handler 中,我们直接调用这些接口函数即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// Handler Struct
type Handler struct {
UserService model.UserService
}
// Handler 初始化结构体
type Config struct {
R *gin.Engine
UserService model.UserService // Service 层接口
}
// 实例化一个 Handler Struct
func NewHandler(c *Config) {
h := &Handler{
UserService: c.UserService,
}
g := c.R.Group(os.Getenv("ACCOUNT_API_URL"))
g.GET("/profile", h.GetUserProfile)
}
// Handler: GetUserProfile
func (h *Handler) GetUserProfile(c *gin.Context) {
...
h.UserService.GetUserProfileById(c, uid)
...
}
注意到上面我们是使用了一个 Config Struct 去初始化 Handler Struct,而 Config Struct 中的 Service 接口,具体是哪一个结构,还是不知道的。这样一来,当我们还没有真的实现业务逻辑的时候,我们可以使用一个假的 Service 层,来对 Handler 的功能进行测试。