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

感觉 uber/fx 并不比 getInstance 工厂好用

  •  
  •   a132811 ·
    ahuigo · 2022-07-21 01:32:02 +08:00 · 2986 次点击
    这是一个创建于 860 天前的主题,其中的信息可能已经有所发展或是发生改变。

    感觉 uber/fx 并不比 getInstance 工厂好用

    不好用的点:

    1. 写单元测试有点麻烦
    2. 需要额外管理属性(这里的属性是:用于存储注入的依赖)
    3. 不完美的按需初始化
    4. 通过反射实现的注入,如果有依赖问题可能要在运行时才能被发现
    5. 性能损失
    6. 调试、维护略复杂(主要是调试 fx 生命周期+反射,调用链就拉得比较长的, 其它都还好)

    下面就前面几点展开解释一下吧(伪代码), 看看 V 友怎么看

    手动维护依赖属性

    fx 只能在顶层方法(app 初始化时)实现自动依赖注入(invoke 调用),非 invoke 调用则不能直接自动注入

    比如要实现调用链server->service->db

    // main.go
    fx.Invoke(func(s *Server){
          s.start()
    })
    

    为了让 service 调用依赖 db, 我们一般需要在顶层用Provide/Module等方法生成一份依赖关系module.Main,比如:

    func NewDb() *Db{
        return initDb()
    }
    func NewService(db *Db) *Service{
        return &Service(db) //注入 db
    }
    func NewServer(s *Service) *Server{
        return &Server(s)   //注入 service
    }
    
    // 用 module 管理维护依赖关系
    module.Main := fx.Module("main",
        fx.Provide(
            NewDb,
            NewService,
            NewServer,
        ),
    )
    

    同时,由于非 invoke 调用不可直接自动注入,所以需要手动增加属性, 用来存储所注入的依赖,比如:

    // server.go
    type struct Server{
        service *Service  //增加 service 属性,用来存储 service 依赖
    }
    
    // service.go
    type struct Service{
        db *Db          // 增加 db 属性,用来存储 db 依赖
    }
    func (s *Service) Insert(){
        //使用 db 依赖
        s.db.Insert()
    }
    

    虽然 New 构造器的编写是一次性的工作,但是对依赖属性的管理,是重复性的工作:

    1. 如果依赖越来越很多,我们所需要手动给每个对象增加的依赖属性就越来越多, 对象会变得越发的臃肿
      • 几乎每一层对象、每一个对象,都要加各种依赖相关的属性(除非它不用依赖), 比如 SerivceA,SerivceB 都要添加 db 属性 重复性工作, 繁琐
    2. 初始化对象时,必须创建所有依赖属性对应的依赖对象(即使可能不会使用依赖属性),这违反了按需初始化的原则(这一点比较影响单元测试效率)。
      • 比如, 因为我们的 Service 依赖 DB, 创建 Service 时就必须先创建 DB 对象, 并把 DB 依赖注入到 Service ,即使不会真正使用到 DB 对象
    3. 我们只能使用 OOP, 而不能使用 Function(因为 Function 不可注入依赖)

    如果使用 getInstance 就不需要手动给 Service 对象增加属性了,也不用受限在 OOP 下,而且可以做到真正的按需要初始化(不使用 DB ,就不会初始化):

    // db.go
    var _inner_db *DB
    func NewDB() *DB{
        if _inner_db == nil{
            _inner_db = connectDB()
        }
        return _inner_db
    }
    
    // service.go
    func (s *Service) Insert(){
        NewDB().Insert() // 直接一行流调用就可以, 且是按需要初始化的,  也可以放到普通函数中调用
    }
    

    写单元测试有点麻烦

    额外的样板代码

    每一处单元测试,都要手动写这么一堆样式代码(fx.New/Module/Invoke)

    func TestXXX(t *testing.T) {
        fx.New(
            module.Service, // 引入 modeule.Service 所有的依赖
            fx.Invoke(func(s *Service) {
                err:=s.Foo()
                // todo test ...
            }),
        )
    }
    

    而我更喜欢简洁的一行流

    func TestXXX(t *testing.T) {
        err := GetInstanceService().Foo()
        // todo test
    }
    

    需要为单元测做额外的依赖管理

    如果在单元测试的孙子、孙孙子函数里面,要调用大量的依赖, 就会比较麻烦(此场景很常见).

    比如下面这个示例中,孙子函数testGetWorkflowDef 依赖到 Workflow 对象

    func TestWorkflow(t *testing.T) {
        fx.New(
            module.Workflow, 
            fx.Invoke(func(workflow *Workflow){
                // 创建 workflow
                wfid,err := testCreateWorkflow(workflow)
                if err!=nil{
                    t.Fatal(err)
                }
                // 完成 workflow
                testFinishWorkflow(wfid)
            }),
        )
    }
    
    // 创建 workflow
    func testCreateWorkflow(workflow *Workflow) (string, err){
        def, err:=testGetWorkflowDef(workflow)
        wfid, err := postCreateWorkflow(def)
        return wfid,err
    }
    
    // 获取 workflow 定义(孙子函数依赖 workflow )
    func testGetWorkflowDef(workflow *Workflow) *WorkflowDef{
        def:=workflow.GenerateWorkflowDef()
        return def
    }
    

    上面示例中,为了将workflow这个依赖传给孙子函数testGetWorkflowDef, 入口方法就将workflow一层一层往下传. 这样做的缺点是: 层数越多、依赖越多,就越麻烦

    为了避免层层传递依赖, 我想到的,就是为单元测试也引入依赖管理:

    1. 首先,可以将单元测试整个调用链testMain->testCreateWorkflow->testGetWorkflowDef,统一放到抽像的对象struct WorkflowTest 中去
    2. 再借助 fx, 为单元测试对象(OOP)单独提供依赖注入

    具体示例如下(避免了上例中的层层传依赖的方式):

    func TestWorkflow(t *testing.T) {
        fx.New(
            module.Workflow, 
            fx.Provide(NewWorkflowTest), // 为单测单独提供依赖
            fx.Invoke(func(workflow *Workflow, wft *WorkflowTest){
                // 创建 workflow
                wfid,err := wft.testCreateWorkflow(workflow)
                if err!=nil{
                    t.Fatal(err)
                }
                // 完成 workflow
                testFinishWorkflow(wfid)
            }),
        )
    }
    
    type struct WorkflowTest{
        workflow *Workflow // 依赖属性
    }
    func NewWorkflowTest(workflow *Workflow) *WorkflowTest{
        return &WorkflowTest{
            workflow: workflow,
        }
    }
    
    // 创建 workflow
    func (wft *WorkflowTest) testCreateWorkflow() (string, err){
        def, err:=wft.testGetWorkflowDef()
        wfid, err := postCreateWorkflow(def)
        return wfid,err
    }
    
    // 获取 workflow 定义(孙子函数依赖 workflow )
    func (wft *WorkflowTest) testGetWorkflowDef() *WorkflowDef{
        def:=wft.workflow.GenerateWorkflowDef()
        return def
    }
    

    可以看到,维护还是比较麻烦

    因为每个含有单元子函数依赖的单元用例,都需要手动维护单独的依赖关系。
    比如,我们如果有其它的测试项,TestTask, TestUser...等等,它们都要像TestWorkflow 那样创建(仅仅为单测)所依赖的对象(struct WorkflowTest),并且在对象中增加属性来存储依赖(像workflow)。

    如果使用 getInstance 就不需要维护依赖关系了,也不需要去添加各种依赖相关的属性了。

    重复维护依赖关系(可以避免,但要花点时间)

    如果想单独调用一个非顶层方法,比如我想做一个针对service.Foo这个方法的 单元测试.

    由于 golang 不能循环依赖,所以不能复用入口函数的依赖定义module.Main (Main->Service->Main 就产生循环了)

    只好重新维护一份依赖关系: module.Service, 然后在单元测试中引入:

    
    // 单独维护一份依赖关系: `module.Service`
    module.Service := fx.Module("service",
        fx.Provide(
            NewDb,
            NewService,
        ),
    )
    
    // 然后在单元测试中引入:`module.Service`
    func TestService(t *testing.T) {
        fx.New(
            module.Service, // 引入 modeule.Service 依赖
            fx.Invoke(func(s *Service) {
                err:=s.Foo()
                if err!=nil{
                    t.Fatal(err)
                }
            }),
        )
    }
    
    

    不过,如果对 fx.Module 做良好的上下分层设计也可以避免重复维护依赖关系,比如:

    1. module.Main 只需要引入 module.Service, 而不必引入 module.DB
    2. 因为 module.Service 已经引入了 module.DB

    这需要在依赖关系的设计上,花点时间

    小结

    个人认为将 fx 用于项目中,收益比起成本,并不太划算。用于低层库则更没有必要,损失性能又增加复杂性

    我想还是 getInstance 工厂更简单精暴省事:

    1. 如果要变更依赖,直接修改它的实现就行了
    2. 如果想支持参数,使用 map 缓存实例就行了
    3. 如果需要并行再加一把 lock
    20 条回复    2022-08-09 22:20:55 +08:00
    dacapoday
        1
    dacapoday  
       2022-07-21 08:42:33 +08:00
    部分赞同楼主观点,在非常微小简单的项目里,getInstance 更合适,更容易定制初始化过程。
    Dogtler
        2
    Dogtler  
       2022-07-21 09:36:16 +08:00
    支持楼主的观点。
    pmx1990
        3
    pmx1990  
       2022-07-21 09:54:23 +08:00
    我现在的项目重度依赖 fx ,整个启动流程都是 fx
    之前也是自己 getInstance ,后来越来越不好维护了
    janxin
        4
    janxin  
       2022-07-21 10:21:51 +08:00
    单测不应该 mock 吗?依赖应该被 mock 掉吧

    其他几个点可以展开说说的。这里面运行时的问题诊断,调试和维护成本都有很多问题可以展开讲讲。

    至于性能损失,这不是什么大问题吧,没人会在 hot path 上用 fx 吧。程序启动时这点性能损失,不是 FaaS 什么的应该都可以忽略
    rickiey
        5
    rickiey  
       2022-07-21 10:59:09 +08:00
    用过,也看别人用过,确实不好用,尤其是看那别人写的,一言难尽
    a132811
        6
    a132811  
    OP
       2022-07-21 14:23:49 +08:00
    @janxin
    有 mock, 但不全 mock ,不追求所有的东西都 mock ,算是介于单测和功能测试之间吧。

    另外,性能损失以及调试问题不大,所以我把这两个痛点放在了最后。


    @pmx1990 我的使用感受跟你不太一样,我是觉得大项目使用 getInstance 工厂来组织代码会比 Fx 好用。
    不知道
    为了避免 getInstance 混乱,需要坚持一些原则:存放实例缓存的命名统一前缀如`_inner_`、缓存变量小写私有、
    工厂名要有统一前缀、实例工厂放在统一固定文件名、严格遵守 package 的上下分层。也能做到 fx.Module 那样清晰组织代码的
    mcfog
        7
    mcfog  
       2022-07-21 20:35:48 +08:00   ❤️ 1
    1. 必要恶,IDE 自动
    2. 恰恰相反依赖注入大大方便了单元测试,走 fx&依赖注入的代码库写单测非常简单,其他同学也说了,mock ,我补充一下,一般情况单测根本不用 fx ,直接创建 mock ,new 你的目标模块开搞
    3. 可以解决甚至很容易解决

    ---

    全局变量 /状态肯定是写起来最快最直接,可是如果要堆代码堆逻辑何苦选 go 呢
    a132811
        8
    a132811  
    OP
       2022-07-23 15:11:55 +08:00
    @mcfog 是否可展开解释一下?

    1. 我例子中的测试代码 TestWorkflow 不用 fx 有更好的方式吗?这个情况我倒是很常用
    2. 用单例工厂 不等于 堆代码堆逻辑 吧?单例工厂把每个实例缓存放在私有变量。即然使用到 fx 管理代码,本身就使用了单例模式(每个依赖就是一个单例)。如果是单例变变例,也是一样的管理。唯一的区别是——单例的缓存实例是放在 package 作用域,fx 是把缓存实例放在 app 内
    3. IDE 只能自动处理一小部分 snippet ,就是我上面说的样板代码(单测 1 ),我写测试时还是要手动维护额外的依赖管理(单测 2 )
    a132811
        9
    a132811  
    OP
       2022-07-23 15:29:27 +08:00
    @a132811 说“唯一的区别”肯定不对。我使用感受是:
    1. fx 提供了生命周期的管理,保证模块加载、退出的有序性。则单纯靠单例工厂,就不容易做。
    2. fx 查看整个项目的执行的依赖比较直观。

    其它区别(优点),我想不到什么了
    blless
        10
    blless  
       2022-07-27 16:18:58 +08:00
    getInstance 实际上就是强行跟代码耦合了,因为你依赖的是一个固定实例而且不可替换。某些代码层级不规范的情况下,很容易产生底层模块依赖上层模块导致循环依赖。写测试的时候必须把整个 GetInstance 的依赖链条都写完才能测试。
    但是 fx 代码注入实际上依赖的只是一个 调用接口 /规范,真正的实例是运行后通过 fx 生成注入的。而且因为依赖注入必须要显示把所有依赖的东西都写到 New 上,某些不必要的依赖就可以 mock 甚至直接 nil 。
    觉得 getInstace 简单,大概率还没碰上 getInstace 的 instance 再 getInstance 再 getInstance 循环这种?
    a132811
        11
    a132811  
    OP
       2022-07-29 20:01:54 +08:00   ❤️ 1
    @blless 不是很同意。

    如果没有做好代码分层,无论是 fx ,还是 getInstance ,都无法避免循环依赖。

    可能和你想的 getInstance 不一样,我说是构造工厂

    1. 我所需要的对象都来自于工厂,调用者不必关心工厂怎么初始化的,也不关心工厂如何实现的、更不关心工厂依赖什么
    2. 调用者**只关心工作的输入输出**符合接口规范。这跟 fx 是一致的。
    3. 如果工厂如果出错了,应该保证返回 err 或者像 fx 那样返回”failed to build xxx“的 panic
    4. 不必要的依赖跟 mock 是两个层面的事情吧。如果是不必要的依赖,最好直接不初始化才对

    不太理解你说的`getInstance 再 getInstance 循环`是什么情况,循环依赖在编译前就会报错不是吗?

    说太多不如上一段实际的代码,拿我们的之前一个项目来说,简化结构如下:

    ```
    app/
    settings/
    fx.go
    config.go
    store/
    fx.go
    cache/
    fx.go
    redlock/
    fx.go
    redlock.go
    redlock_test.go
    pg/
    es/
    kafka/
    ....
    service/
    service1/
    service2/
    ....
    main.go
    ```

    虽然引入了 fx, 但是加功能测试`redlock_test`时就发现有循环依赖:
    ```
    $ cat ./store/cache/redlock/redlock_test.go
    func TestRedlock(t *testing.T) {
    fx.New(
    settings.Module,
    fx.Provide(xxx),
    ....
    }
    $ go test -v ./store/cache/redlock/
    package app/store/cached/redlock
    imports app/store
    imports app/service
    imports app/settings: import cycle not allowed in test
    FAIL app/store/cache/redlock [setup failed]
    ```

    原因是, 因为 app/settings/fx.go 包含了依赖了各种配置的构造器

    ```
    import (
    "app/service"
    "app/store/pg"
    "app/store/es"
    "app/service/..."
    ...
    )


    // yaml 文件配置解析
    func GetConfig() (config *Config) {
    ...
    }

    // 各种配置的构造器
    func NewRedisUniversalOptions(config *Config) *redis.UniversalOptions {... }
    func NewPostgresOptions(config *Config) *pg.Options {...}
    func NewEsOptions(config *Config) *es.Options {...}
    func NewService1Options(config *Config) *srv1.Options {...}
    ....

    var Module = fx.Module("settings",
    fx.Provide(
    GetConfig,
    NewRedisUniversalOptions,
    NewPgOptions,
    NewEsOptions,
    NewService1Config,
    ....
    ),
    )
    ```

    可以看到
    1. setting 基础配置,依赖其它种 store(rddis/es/pg/kafka...),甚至 service 的 Options 类
    2. setting 是其它库的上层,而不是底层。只有顶层的 main.go 可调用 setting ,其它不行
    3. 我们就不能在其它包内引用这个 setting, 想引用的话就需要重新对配置解耦合。

    还是那句话,无论是工厂还是 fx ,代码如果没有做好良好的解耦分层,项目都不会那么好维护。
    YuuuuuuH
        12
    YuuuuuuH  
       2022-08-03 20:20:29 +08:00
    >> 每一处单元测试,都要手动写这么一堆样式代码(fx.New/Module/Invoke)

    @a132811 这里为什么不用 setUp 函数来初始化测试固件呢?
    a132811
        13
    a132811  
    OP
       2022-08-04 12:50:15 +08:00
    @YuuuuuuH 这里其实就是 setUp 函数,初始化了 fx.New/Module/Invoke 。

    不过,由于需要的 provide 依赖不同,所以需要写各种 setUp 函数。
    kkhaike
        14
    kkhaike  
       2022-08-04 19:15:51 +08:00   ❤️ 1
    我使用 fx 大概两年左右,下面就针对楼主各点给出我的看法。。

    PS1. 新出的 fx.Module 还没想到用得到地方,可能我项目不够大吧。

    # 手动维护依赖属性
    使用 fx.In 参数对象解决依赖问题,我一般一个依赖一个包,所以都是这样写的 New 护士

    ```
    // dep 专门用来给 fx 注入参数
    type dep struct {
    fx.in

    dao dao.M // 为了单测方便,数据层的一般都提供 interface
    busi busi.M
    }

    // 这里看这个模块的情况,想被 mock 就写
    // type M interface {
    // bala()
    // }
    type M = *module

    type module struct {
    *dep // 内嵌指针就好了

    some int // 内部变量
    }

    // 私有参数,让其无法手动调用,只用来 fx
    func New(d dep) M {
    return &module(dep: &dep)
    }

    ```

    server->service->db Provide 定义问题,因为我每个 Provide 都分包的,
    所以要增加一个 fx.Invoke 去注册 service 到 server

    所以,Server 是单独 New 出 Provide 的,无依赖(他被人依赖)
    所以就有三个 fx.Provide
    1. dao.New 无依赖
    2. service.New 依赖 dao
    3. server.New 无依赖
    两个 fx.Invoke
    4. service.Reg 依赖 server, service ,实现 server.Reg(service)
    2. server.Life 注册 server 启停,注意最好用 fx.Lifecycle 注册,不要直接调用

    如果嫌弃一个 service 定义两个 Option 很麻烦的话,
    实际上,service.M 基本不会被其他 Provide 使用,所以也可以省略 service.New ,
    直接在 service.Reg 中完成,New 和 绑定操作

    可以看出。实际上加属性,也只需要修改一个地方,即 dep 成员,整个也很简单

    # 写单元测试有点麻烦
    与楼主相反,我觉得倒是写 UT 非常简单。。

    由于我按每个 Provide 分包,所以 UT 分为 Provide 内 UT ,Provide 间 UT

    Provide 内 UT ,直接 new 对象,按需初始化成员就好了,不要走 New 函数创建对象,
    因为 和其他的 Provide 内 无关,所以 UT 根本不需要 fx 介入,比如我上面依赖 dao.M 的例子
    可以直接创建该成员 mock 对象

    Provide 间 UT ,由于 fx 的特殊原因,以及 go 的循环引用问题,所以 Provide 间 UT 只能用在
    黑盒测试,我是这么处理的

    有一个 app 模块 管理所有 Provide/Invoke ,main 直接调用 app.App().Start() 启动程序

    ```
    func TestXXX(t *testing.T) {
    var m some.M
    defer fxtest.New(t, app.Provides(), fx.Populate(&m)).RequireStart().RequireStop()
    }
    ```

    你还可以用新出的 fx.Replace 替换链上的依赖为 mock

    # 其他点
    5. 性能损失:fx 只作用于你程序的初始化阶段。。。这个阶段谈性能损失?
    6. 调试、维护略复杂,你可能是没用 fx.Logger ,我用了 2 年都没有调试过 fx
    kkhaike
        15
    kkhaike  
       2022-08-04 19:26:30 +08:00
    另外我在说说 fx. Lifecycle ,这个东西是你 getInstance 怎么都做不到的。
    想象一下一个几十个 Provide 瞬间无措优雅关停的快感。。

    什么,你们你们不优雅关停。。那么当我多说了
    kkhaike
        16
    kkhaike  
       2022-08-04 19:31:47 +08:00
    另外我上面说的黑盒测试补充一下。。。
    由于 go 循环引用的原因,一般黑盒测试是定义在被测包同层文件目录中,
    包名是 xxx_test ,

    比如 xxx 目录为 package xxx ,他下面有
    xxx.go 包名是 xxx
    xxx_test.go 白盒,包名是 xxx
    xxx_bb_test.go 黑盒,包名是 xxx_test
    kkhaike
        17
    kkhaike  
       2022-08-04 19:40:07 +08:00   ❤️ 1
    fx 的初始化逻辑简单概括就是
    1. 初始化所有 fx.Invoke 的所有依赖
    2. 顺执行 fx.Invoke

    所以 fx 最佳实践就是用一个包管理所有依赖,因为他会按需创建

    我上面的 Provide 间 UT 例子,他就只会创建你 Populate 对象所有依赖

    另外。。。我的 app 模块甚至都是 go generate 生成的。。
    甚至,上面我举例的 dep 依赖结构都是生成的。。

    我模块代码只有
    ```

    type M = *module

    type module struct {
    *dep
    }

    //go:generate bb gen $GOFILE
    // @dep(dao)
    func New(d dep) (_ M, err error) {
    }
    ```
    a132811
        18
    a132811  
    OP
       2022-08-06 17:27:25 +08:00
    @kkhaike 感谢你的回复。

    你给的 fx 用例—— 说明了 fx 能提供很方便的自动依赖注入、lifecycle 、按需创建等等(用 fx.Module 还可以进一步实现对复杂依赖的模块化管理)

    但是回避了我说的问题(这取决于你是否这认为这些是问题,如果你认为这些问题可以忽略,那么 fx 自然是很好用; 而我认为这些问题较为重要,大多数情况可通过构造工厂避免,并且不降低项目的可维护性)

    我描述的场景,都属于白盒,需要从内部进行,而不是在外围顶层运行:
    `fxtest.New(t, app.Provides(), fx.Populate(&m)).RequireStart().RequireStop()`


    1. 手动维护依赖属性的场景
    >> 可以看出。实际上加属性,也只需要修改一个地方,即 dep 成员,整个也很简单"
    你描述的用法属于单个 fx.New 的场景,只需要维护一份依赖关系就行了。

    但是我的场景是:**由于要测试、临时运行局部函数**, 此时就需要创建很多不同的 fx.New

    **为什么不能避免重新创建 fx.New 呢?**
    因为要运行的是局部代码,我想把测试本身也放在局部,局部代码的依赖关系也不一样。所以就不能复用入口层 main.go 中的 fx.New()

    fx.In/Out/module 确实能方便的组织依赖关系,但是手动管理 provides 是不能省掉的,用工厂就省掉这一步的。


    2. **不完美**的按需创建
    fx 虽然提供了注入时才自动创建,但是不能做到 **更完美**的按需创建。

    比如,我们使用 fx 为 Service 注入依赖 DB, 创建 Service 时 fx 会自动创建并注入 DB 对象,即使不会真正使用到 DB 对象.

    为什么明明依赖了 db 对象,却又不使用它呢?

    因为我只是想调试一下单个的方法 Service.FetchApi() ,它并不需要 db 对象,却必须初始化不相关各种 db 对象(有的 db 连接还限制频率、连接失败的问题)

    当然了,可以 mock db 嘛,但是有几个问题:只能在注入前 mock 而不能注入后再 mock; 有些 library 并没有暴露私有属性的 mock 方法; 有些复杂的库自身也没有 mock 的能力,比如 uber cadence 并没有 mock-cadene-client 这种东西。

    真实的情况,需要 mock 的东西可能会很复杂,如果去研究怎么 mock 就很浪费时间。

    3. 单元测试有点麻烦
    你提到的
    >> Provide 间 UT ,由于 fx 的特殊原因,以及 go 的循环引用问题,所以 Provide 间 UT 只能用在
    黑盒测试,我是这么处理的
    >> 有一个 app 模块 管理所有 Provide/Invoke ,main 直接调用 app.App().Start() 启动程序
    >> ```
    >> func TestXXX(t *testing.T) {
    >> var m some.M
    >> defer fxtest.New(t, app.Provides(), fx.Populate(&m)).RequireStart().RequireStop()
    >> }

    你这份代码,是在外围顶层 main.go 中建了一份`fxtest.New` 然后 start 运行测试代码。
    缺点是:1. 无法测试的局部代码的私有方法 2.需要手动维护测试所需的`app.Provides()`, 3. 外部的 invoke 与内部的被测代码是分离的,所以必须先把被测代码 import 出来。4. 我更喜欢`model/{user.go, user_test.go}` 测试与代码放到一起,并且不需要在外围引入`fxtest.New().Start()`.

    我得重复一下问题——fx 确实能避免手动 New ,但是不能避免**手动管理**依赖关系.

    我在本文描述中提到的单测 case 就是这样的,简单的说就是:

    第一,要手写不同测试所需的不同的 provide ,第二, 要定义一些私有属性来存放注入的对象(复杂测试也需要有专属的依赖注入)

    依赖越多,手动写的工作量就就越多.

    在 go 下,这种情况把 OOP 换成函数式,此类手动管理的代码,可不需要写,会更简单

    4. Lifecycle
    一般情况我对 lifecycle 没有特别高要求,需要关停的就那么几个,平滑退出可直接在入口层统一处理

    //main.go
    func main(){
    .....
    defer close()
    <-ctx.Done()
    }
    func close(){
    if PgFactory.Exists(): PgFactory.Close()
    ....
    EsFactory.Close()
    KafkaFactory.Close()
    closeHttpServer()
    ....
    }

    也可以自定义 context, 把所有要释放的方法注入到全局的 ctx, 然后用 cancel 或者 ctx.Cancel()触发释放


    // main.go
    serverCtx, cancel := WithCancelFns(context.Background())
    ....
    defer cancel()

    // service.go
    getServerCtx().AddCancelFns(
    func() { println("close pg") },
    func() { println("close server") },
    )

    5. go generate 生成的
    >> app 模块甚至都是 go generate 生成的
    并不清楚你的 app 是什么实现。
    我的经验是 fxTest 初始化的样板代码,我可以用 ide/vscode 的 snippet 一键一生成,只是不同的依赖管理+属性的初始化,不能抽象为模板,还是要手写

    6. 循环引用
    之所提这个问题,只是为了回应前一位 v 友——循环问题只跟代码结构设计有关,跟 fx 本身无关
    > ps, v 站不支持回复加 markdown 缩进全被吃了:)

    7. 性能损失
    赞同你说的, 确实不关紧要

    8. fx 调试
    正常调用是没有问题,除了特例:

    比如依赖 /接口是自动注入的,当我想了解这个接口到底是哪里初始化的,是需要先了解整个项目的依赖关系图的。而单例工厂则是显式的、直观的。(当处理一个不熟悉的复杂项目时,就会有这个感受)
    kkhaike
        19
    kkhaike  
       2022-08-07 12:01:59 +08:00
    关于黑盒测试
    1. 上面我也补充了,不需要放在顶层,放在你待测 Provide 目录中就行就行,你可能没看到吧。。在 16 层
    2. 私有函数问题,你可以试着封装 2 个 interface
    M interface -> 外部暴露函数
    P interface -> 黑盒测试私有函数
    你的 module 对象是实现 M + P, New 只提供 M
    这样的话 你在 Populate 出 M 的时候,可以通过 m.(P) 转成泥似的私有 interface 进行调用。
    3. 不需要的模块,以及需要切换成 mock 的模块,我在上面说了可以通过 Replace 替换掉

    总之,总体目标就是不要为每一个 Provide 间 测试构造你的依赖,问题总有解决方法

    关于生成
    1. 我的生成框架是识别 New 函数上的 @dep 注解,
    然后在 app 包里添加对应的 Provide ,以及生成对应的 dep 参数对象,
    是为了解决你说的维护 Provide 问题,整个框架只需要修改 @dep 注解就好。
    a132811
        20
    a132811  
    OP
       2022-08-09 22:20:55 +08:00
    抱歉,你说的让我有点疑惑。

    >> 1. 上面我也补充了,不需要放在顶层,放在你待测 Provide 目录中就行就行,你可能没看到吧。。在 16 层
    在 16 层,你说的是避免循环引用吧?

    拿你说的同层目录吧,model/{user.go,user_test.go} 都是放在 model 这个目录。
    不还是需要在 model/user_test.go 中创建新的 fx.New(Provide) 吗?怎么能避免创建 fx.New(Provide)呢?

    >>2. 私有函数问题,你可以试着封装 2 个 interface 。
    >>M interface -> 外部暴露函数
    >>P interface -> 黑盒测试私有函数

    不太明白你说的黑盒测试私有函数是指什么?

    既然是私有函数、方法,那它就不可以暴露给外部(如果能暴露给外部,那它就不应该是私有的)

    既然是黑盒测试,那应该是从包的外部访问。既然从外部访问,那只能访问公有函数,而无法访问私有函数吧。

    除非,你在内部写一个方法,将私有函数暴露出去(这就属于公有函数了)。

    其实我并没有必要外部暴露它呀,如果我想运行这个私有函数,我只需要在同层目录放个 xxx_test.go 就可以访问运行它呀,多简单不是。
    -------------
    或许,我理解错了,你是指的别的吗?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2500 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 16:00 · PVG 00:00 · LAX 08:00 · JFK 11:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.