写得比较长,🙏
问题是这样的,A 层依赖于 B 层,A 作为使用者想隔离 B 的依赖,定义 interface 的时候该如何设计?
比如:controller 层从 JSON 绑定 model ,后续需要调用 service 层的用户处理逻辑
代码如下:
// package main
// 构造 user service 实例
userService := service.NewUserService(...)
// userApp 依赖 user service
userApp := controller.NewUserApp(userService)
-----
// package controller
type UserApp struct {
svc *service.UserService
}
type User struct {
...
}
func NewUserApp(svc *service.UserService) *UserApp {
return &UserApp{svc: svc}
}
// handler
func (app* UserApp) Add(c Context) error {
// controller 层绑定 model
var user User
if err := bindJSON(c, &user); err != nil {
return err
}
// model 转换
var svcUser service.User
toSvcUser(user, &svcUser);
// 调用 service 逻辑
return app.svc.AddUser(svcUser)
}
----
// package service
type UserService struct {
...
}
func NewUserService(...) *UserService {
...
}
这里为了方便测试 controller ,希望把 service 的依赖作为一个接口,变成下面这样:
// package controller
type IUserService interface {
AddUser(user xxx) error
}
type UserApp struct {
svc IUserService
}
func NewUserApp(svc IUserService) *UserApp {
return &UserApp{svc: svc}
}
我的疑问是这里的接口应该是下面的哪一种:
方法 1:
type IUserService interface {
// 使用自己包的类型
AddUser(u User)
}
方法 2:
type IUserService interface {
// 使用依赖包的类型
AddUser(u service.User)
}
方法 3:
另开一个 models 包。把类型全塞 models 包里面,controller 层和 service 层都依赖于 models
这种做法似乎不推荐吗?
https://rakyll.org/style-packages/
如果是第一种的话,我是不是还需要在 controller 层加一个 adapter 来适配接口:
// package controller
type UserServiceAdapter struct {
svc *service.UserService
}
// adapter 实现接口
func (adapter *UserServiceAdapter) AddUser(user User) error {
// model 转换
var svcUser service.User
toSvcUser(user, &svcUser);
// 调用 service 逻辑
return adapter.svc.AddUser(svcUser)
}
func NewUserServiceAdapter(svc *service.UserSvC) *UserServiceAdapter{
return &UserServiceAdapter{ svc: svc }
}
// package
// 构造 user service 实例
userService := service.NewUserService(...)
userApp := controller.NewUserApp(
// 创建 adapter ,adapter 满足接口
controller.NewUserServiceAdapter(userService)
)
相关参考链接:
https://tutorialedge.net/golang/accept-interfaces-return-structs/
1
chotow 2022-07-03 14:30:36 +08:00 1
抛砖引玉,谈谈我的个人意见
AddUser 依赖的 User ,不属于 controller 也不属于 service ;我常用的做法是把 User 放到 model 下; controller 负责把 Raw Data 转为业务模型,再传给 service model 下的这些模型,有的模型是数据库模型,那么会顺便为其配置相关 Tag ,包括但不限于 gorm Tag ;有的模型只是功能模型、中间层模型,比较简单 除了 model ,还有一种场景是在 proto 里定义 Message ,这种情况其实和 model 也类似,可以简单视为换了个包名 结论:如果是某一层专用的结构体,会单独放在这一层下;如果是全局通用的结构体,一般放在 model 下;如果是跨项目通用的结构体,放在 proto 下;其中,第二、三种我用的时候没有很强势区分分界线(虽然感觉并不太好) |
2
wheeler OP @chotow 谢谢。
我也看到过有人在 model 里面复用一个 user model ,然后加上各种 tag 比如 gorm tag 、json tag 、swagger tag ,一旦复杂了管理起来就麻烦了。感觉还是各种 vo dto 转比较好。 |
3
AnroZ 2022-07-03 15:01:50 +08:00 1
@wheeler 嗯,如果按照 Golang 的习惯可能是不建议你这么抽象的,因为 model 类型多了写起来太麻烦了。但,如果按照你的思路,我觉得还是直接定义到 models 包较好。 另外,要考虑到 Golang interface 机制对代码阅读不友好,尽量不要设计太多的接口,绕来绕去。
|
4
chotow 2022-07-03 15:05:44 +08:00 1
重新看了下楼主提到的 https://rakyll.org/style-packages/ ,里边的指导也是有道理的;统一的 model 对 go 来说不太好——这个设计我确实是从 php 带过来的 🐶
按功能划分的话,如文章所述,User 是放在 service 下的,那就应该是和对应的接口定义在一块,看起来也没毛病;那么需要考虑的是会不会出现跨 service 引用模型,实际业务里已经有这种场景存在了,对此,难道就放到 proto 里?似乎可以的样子 🤔 --- #2 VO 、DTO 这种概念应该是来源于 Java 吧?我比较反感这些。类似的还有接口的定义( I 前缀),接口的实现( IMPL 前缀或后缀)。有段时间我也给接口加了 I 前缀,后来一次迭代里又全部删掉了,随 Go 标准库,根据实际情况用上 er 后缀 😂 |
5
wheeler OP |
6
WilliamYang 2022-07-04 11:24:20 +08:00
@chotow 我也拿捏不好 pb 与 model 的界线,如果经常混用,又要写一堆 ToPB() 方法,这个怎么看
|
7
chotow 2022-07-08 13:14:53 +08:00
@WilliamYang 我目前是把公共的放 pb ,方便多处引用
|