V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
wheeler
V2EX  ›  Go 编程语言

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

  •  
  •   wheeler · 2022-07-03 13:49:17 +08:00 · 2036 次点击
    这是一个创建于 908 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写得比较长,🙏

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

    7 条回复    2022-07-08 13:14:53 +08:00
    chotow
        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 下;其中,第二、三种我用的时候没有很强势区分分界线(虽然感觉并不太好)
    wheeler
        2
    wheeler  
    OP
       2022-07-03 14:51:36 +08:00 via iPhone
    @chotow 谢谢。

    我也看到过有人在 model 里面复用一个 user model ,然后加上各种 tag 比如 gorm tag 、json tag 、swagger tag ,一旦复杂了管理起来就麻烦了。感觉还是各种 vo dto 转比较好。
    AnroZ
        3
    AnroZ  
       2022-07-03 15:01:50 +08:00   ❤️ 1
    @wheeler 嗯,如果按照 Golang 的习惯可能是不建议你这么抽象的,因为 model 类型多了写起来太麻烦了。但,如果按照你的思路,我觉得还是直接定义到 models 包较好。 另外,要考虑到 Golang interface 机制对代码阅读不友好,尽量不要设计太多的接口,绕来绕去。
    chotow
        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 后缀 😂
    wheeler
        5
    wheeler  
    OP
       2022-07-03 15:50:59 +08:00 via iPhone
    @chotow 如果按照功能垂直分层的话,很可能出现 user 包和别的功能包循环引用的情况。

    我后面找些开源代码学习下。
    WilliamYang
        6
    WilliamYang  
       2022-07-04 11:24:20 +08:00
    @chotow 我也拿捏不好 pb 与 model 的界线,如果经常混用,又要写一堆 ToPB() 方法,这个怎么看
    chotow
        7
    chotow  
       2022-07-08 13:14:53 +08:00
    @WilliamYang 我目前是把公共的放 pb ,方便多处引用
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1178 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 18:26 · PVG 02:26 · LAX 10:26 · JFK 13:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.