请教 golang 依赖注入的实际问题

28 天前
 chaleaochexist

我有一个 task 通过 ssh 运行 N 种命令, 假设 ls -lcat /etc/host吧. 然后把输出存起来.

理想中的结构是:

task 依赖 handler 依赖 sshclient

其中,

handler 是函数, 参数是 sshclient, 一个 handler 执行一种命令

sshclient 是接口, 干活的.

这个 sshclient 实例化过程只能在 task 中动态生成, 因为 sshclient 需要的 ip 是在 task 中的其他函数获取的.

我得问题:

  1. task 依赖 handler. 但是 handler 的参数的类型(也就是 sshclient) 定义在 task 中, 这不循环导入了吗?

目前的解决方案是, 我在 handler 和 task 中分别定义两个一模一样的接口, 然后通过适配的方式能让代码运行. 我不确定这样处理是否合理? 还是说我这个设计本身就有问题? 通过注入接口能实现吗?

  1. 当一个接口的实现的依赖是动态数据的时候, (譬如 sshclient 中的 ip, 端口, 认证信息), 还需要注入吗? 如何注入? 我目前采用的方案是注入一个无参的返回值是工厂函数的函数...然后再 task 中实例化 sshclient. 补充: sshclient 可能有 100 个, 不是一个固定的 client. 说白了我是通过 ssh 采集信息的. 和 *db.DB 不是一个类型.

目前有点混乱, 如果我没问清楚欢迎各位大佬提出你的疑问 我尽量补充信息.

2300 次点击
所在节点    Go 编程语言
44 条回复
chaleaochexist
27 天前
@NessajCN

#3 的例子中 client.connect(ip) 的用法和 NewSshClient(ip) 没有本质区别.
实际上我第一版就是类似 client.connect(ip)的写法.

"sshClient 显然是个实例,咋会是接口呢。" 在我 github 的例子中 sshClient 是接口
https://github.com/chaleaoch/golang_demo/blob/master/v1/internal/task/a.go#L22

"都说了接口不是这么用."
接口实现了鸭子类型.
然后你说我得哪个接口用的不对是指 provider 吗? 通过 provider.GetFactory 拿到了一个 sshClient 的工厂函数, 然后再实例化这个 sshclient 是不对的, 应该直接传 sshclient 然后 sshclient.connect(ip) 是这个意思吗? 我没觉得这俩有本质区别啊.

但是我得例子中的问题不在这里, 而是引入了 handler 之后, 在哪里 定义接口的问题.
chaleaochexist
27 天前
@NessajCN 不过还是谢谢你耐心和我讨论问题... 希望你能和我继续 讨论.
chaleaochexist
27 天前
实际上我第一版 --> 在我提供的 github 仓库中没有体现, 是我之前在工作中的第一版.
chaleaochexist
27 天前
@NessajCN 别着急大佬 等我再写一个版本 和你一起讨论.
chaleaochexist
27 天前
@NessajCN 我猜 https://github.com/chaleaoch/golang_demo/tree/master/v4
这个文件夹下的代码 比较符合你的品味吧?

但是我得问题是, 如果我想注入 handler? 要如何做?
答案是不是: handler 就不应该注入 而是直接调用?

实际上我也是这么做的, 但是我需要你们 确认一下!!! 这回 我说明白了吗?
NessajCN
27 天前
@chaleaochexist
「在我 github 的例子中 sshClient 是接口」
所以我一直说你理解错 go 里 interface 的用法了呀....orz
「但是我得例子中的问题不在这里, 而是引入了 handler 之后, 在哪里 定义接口的问题.」
你这句话问得就错了,接口的定义仅仅是简单的
type If interface {
func1()
func2()
}
你想表达的是这里面的 func1() func2() 具体怎么定义对吧?
那不叫定义接口,而是给某个 struct 实现 (implement) 。
你的 handler 里这种写法

func Cmd1Handler(sshClient SSHClient) string {
out, _ := sshClient.ExecuteCommand("cmd1")
// 干点别的...100 行
return out
}

func Cmd2Handler(sshClient SSHClient) string {
out, _ := sshClient.ExecuteCommand("cmd2")
// 干点别的...100 行
return out
}

意图显然是把 sshClient 作为一个实例而不是接口参数
所以我提到你需要把 SSHClient 定义成 struct 而不是 interface
然后这么定义相应的 handler 方法
func (sc *SSHClient) CmdHandler(cmd Command) string {}
chaleaochexist
27 天前
@NessajCN 要么是我没说清楚, 要么是你没仔细看我得代码.

sshClient 是接口, 因为我现在的实现是 standardSshClient 将来可能基于第三方库去实现这个 sshclient. 所以 sshclient 需要定义成一个接口. 这是原因 1. 原因 2 如果 sshclient 是一个具体的结构体实现, 那么将来如何 mock? 如何做单元测试? 单元测试的时候, 需要脱机测试.

[意图显然是把 sshClient 作为一个实例而不是接口参数]
func Cmd1Handler(sshClient SSHClient) string {
的意图就是接口, 而不是实例.

原因是喜闻乐见的, Accept interfaces,return structs

至于为什么 CmdHandler 是一个函数而不是 sshclient 的方法. 是因为 除了我前面说的 sshclient 是一个接口还有一个原因是, 实际上情况比 demo 要复杂一点点除了 sshclient 还有 httpclient,
client 只负责执行, 而不考虑业务, CmdHandler 是带业务的, 就是我说的, 省略 100 行的内容.

==============================分隔
我得最后一个问题, 大佬帮忙看一下 v4 是否符合逻辑呢?
NessajCN
27 天前
@chaleaochexist
合逻辑呀
虽然要我来 code review 的话 SSHClient 这些接口定义都不需要只留 StandardSSH 就行了
不过你说除了 StandardSSH 之外还有其他 struct 需要传给 CmdHandler 那确实可以用 interface
这样的话你需要把 Connect(username string, password string, host string) error 加到 SSHClient 的接口定义里
chaleaochexist
27 天前
@NessajCN 你平时肯定不做单元测试. 我确定.
NessajCN
27 天前
@chaleaochexist
SSHClient 是 struct 就不能写脱机测试了又是咋得出的结论呀...
chaleaochexist
27 天前
@NessajCN 没办法模拟, 反正我不会...
NessajCN
27 天前
@chaleaochexist
写测试更用不到 interface 了
哪怕你代码里用 interface 作为参数定义函数
实际测试里也肯定是传的实现了那个接口的 struct
不理解你因为需要单元测验而必须定义 interface 的原理
chaleaochexist
27 天前
@NessajCN 你写个测试就知道了.
譬如 repo 访问数据库吧 现在需要你 在没有数据库的情况下测试 service 层的函数

如何 mock 假数据.
只有接口可以做到.

你传入一个结构体 结构体的依赖是 db.DB 他可是真的通过 TCP 去连数据库.

但是如果是接口 我就可以做一个假的 struct 去实现这个接口, 然后返回假数据就行了.

总之一句话 你尝试给你的 repo 层 写单元测试就明白了.
sthwrong
27 天前
依赖就分错了,provider 依赖 repo , 提供方法返回 clients ,handler 依赖 clients ,提供方法根据传入 clients 和 cmd 构建单个或者多个 handler ,task 依赖 handler ,提供方法执行 handler ,每个实现自己声明一个接口 。依赖清晰了,每一层都可以在 test 中声明新的 mock 实例实现 mock 方法替代调用。
NessajCN
27 天前
@chaleaochexist

fakeData := DbData{}
FunctionToBeTested(fakeData)
这里头有接口啥事啊.....
你的意思是 DbData{} 这个数据结构体初始化的时候必须连数据库?
sthwrong
27 天前
每层依赖做好的话,单测可以这样 mock 调用
chaleaochexist
27 天前
@NessajCN 啊? 我没跟上.. 啥意思?

我得意思是
```

type Repo struct {
db *sql.DB
}

func (r *Repo) GetUserByID(userID int) (*User, error) {
// ...
}

type UserService struct {
repo Repo
}

func NewService(repo Repo) *UserService {
return &UserService{repo: repo}
}

func (s *UserService) FindUserByID(userID int) (*User, error) {
user, err := s.repo.GetUserByID(userID)
if err != nil {
return nil, fmt.Errorf("service error: failed to find user with ID %d: %w", userID, err)
}
// 在这里可以添加业务逻辑
return user, nil
}

```

现在的要求是 1. 针对 FindUserByID 做单元测试. 2. 没有数据库 要求 mock 假数据.
你试试吧. 不算为难你吧.


我得问题是: 第一步 你需要实例化 NewService 你传什么参数进去? 你传一个真的, 那一定有一个真正的 db 连接, 传一个假的 编译失败.
chaleaochexist
27 天前
@sthwrong 明白你的意思 你把 repo 放 provider 里了 就迎刃而解了. 这样就不需要 动态初始化了是吧.

逻辑上是的, 但是不符合业务逻辑吖...
NessajCN
27 天前
@chaleaochexist 你这么写就没打算让人能单独传假数据测啊…


// 在这里添加业务逻辑

这部分单独做成函数,把 user 当参数传进去,然后做 user 的假数据就行了
sthwrong
27 天前
@chaleaochexist #38 动态数据本身就是来自 repo 啊,生成动态结果就依赖 repo 给数据,明显的依赖关系。依赖层级对了,每层都可以决定是否需要 interface ,想 mock 的都可以,调用层依赖这个 interface ,test 的时候自己实现个 mock 实例传进去。

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

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

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

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

© 2021 V2EX