请教一个简单的 Golang interface 和 models 组织的问题

2022-07-03 13:49:17 +08:00
 wheeler

写得比较长,🙏

问题是这样的,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/

https://github.com/golang/go/wiki/CodeReviewComments

2037 次点击
所在节点    Go 编程语言
7 条回复
chotow
2022-07-03 14:30:36 +08:00
抛砖引玉,谈谈我的个人意见
AddUser 依赖的 User ,不属于 controller 也不属于 service ;我常用的做法是把 User 放到 model 下; controller 负责把 Raw Data 转为业务模型,再传给 service
model 下的这些模型,有的模型是数据库模型,那么会顺便为其配置相关 Tag ,包括但不限于 gorm Tag ;有的模型只是功能模型、中间层模型,比较简单
除了 model ,还有一种场景是在 proto 里定义 Message ,这种情况其实和 model 也类似,可以简单视为换了个包名
结论:如果是某一层专用的结构体,会单独放在这一层下;如果是全局通用的结构体,一般放在 model 下;如果是跨项目通用的结构体,放在 proto 下;其中,第二、三种我用的时候没有很强势区分分界线(虽然感觉并不太好)
wheeler
2022-07-03 14:51:36 +08:00
@chotow 谢谢。

我也看到过有人在 model 里面复用一个 user model ,然后加上各种 tag 比如 gorm tag 、json tag 、swagger tag ,一旦复杂了管理起来就麻烦了。感觉还是各种 vo dto 转比较好。
AnroZ
2022-07-03 15:01:50 +08:00
@wheeler 嗯,如果按照 Golang 的习惯可能是不建议你这么抽象的,因为 model 类型多了写起来太麻烦了。但,如果按照你的思路,我觉得还是直接定义到 models 包较好。 另外,要考虑到 Golang interface 机制对代码阅读不友好,尽量不要设计太多的接口,绕来绕去。
chotow
2022-07-03 15:05:44 +08:00
重新看了下楼主提到的 https://rakyll.org/style-packages/ ,里边的指导也是有道理的;统一的 model 对 go 来说不太好——这个设计我确实是从 php 带过来的 🐶
按功能划分的话,如文章所述,User 是放在 service 下的,那就应该是和对应的接口定义在一块,看起来也没毛病;那么需要考虑的是会不会出现跨 service 引用模型,实际业务里已经有这种场景存在了,对此,难道就放到 proto 里?似乎可以的样子 🤔
---
#2 VO 、DTO 这种概念应该是来源于 Java 吧?我比较反感这些。类似的还有接口的定义( I 前缀),接口的实现( IMPL 前缀或后缀)。有段时间我也给接口加了 I 前缀,后来一次迭代里又全部删掉了,随 Go 标准库,根据实际情况用上 er 后缀 😂
wheeler
2022-07-03 15:50:59 +08:00
@chotow 如果按照功能垂直分层的话,很可能出现 user 包和别的功能包循环引用的情况。

我后面找些开源代码学习下。
WilliamYang
2022-07-04 11:24:20 +08:00
@chotow 我也拿捏不好 pb 与 model 的界线,如果经常混用,又要写一堆 ToPB() 方法,这个怎么看
chotow
2022-07-08 13:14:53 +08:00
@WilliamYang 我目前是把公共的放 pb ,方便多处引用

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/863748

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX