我有一个 task 通过 ssh 运行 N 种命令, 假设 ls -l
和 cat /etc/host
吧. 然后把输出存起来.
理想中的结构是:
task 依赖 handler 依赖 sshclient
其中,
handler 是函数, 参数是 sshclient, 一个 handler 执行一种命令
sshclient 是接口, 干活的.
这个 sshclient 实例化过程只能在 task 中动态生成, 因为 sshclient 需要的 ip 是在 task 中的其他函数获取的.
我得问题:
目前的解决方案是, 我在 handler 和 task 中分别定义两个一模一样的接口, 然后通过适配的方式能让代码运行. 我不确定这样处理是否合理? 还是说我这个设计本身就有问题? 通过注入接口能实现吗?
目前有点混乱, 如果我没问清楚欢迎各位大佬提出你的疑问 我尽量补充信息.
1
sunny352787 23 天前 ![]() 参数类型为啥定义在 task ?不是应该定义在 handler 吗?
|
2
Dorathea 23 天前 ![]() "因为 sshclient 需要的 ip 是在 task 中的其他函数获取的"
能否将这个依赖剥离出来, 作为 sshclient 的依赖呢? 如果 task 也需要这个, 让 task 也依赖就好了. 以及"循环导入" 的问题, 我之前接触过 nestjs, 也好奇过循环依赖如何解决 https://docs.nestjs.com/fundamentals/circular-dependency 或许可以另一种思路参考 |
3
NessajCN 23 天前 ![]() 「这个 sshclient 实例化过程只能在 task 中动态生成, 因为 sshclient 需要的 ip 是在 task 中的其他函数获取的.」
没看懂这前后两句话的因果 你建一个 sshclient 的实例,然后在 task 的时候调不就行了。把 ip 当作参数传进去 譬如 sc := NewSshClient() func task(client *SshClient) { ip := GetIp() client.connect(ip) } task(&sc) |
![]() |
4
chaleaochexist OP @NessajCN 因为注入嘛, 我在注入的时候(也就是初始化的时候) 是不知道 ip 的.
我现在的代码和你的例子差不多, 只不过不是用的 connect 而是注入了一个无参的函数, 这个函数返回一个工厂函数, 这个工厂函数返回 sshclient. 如果 sshclient 的 ip 是静态的类似 db.DB, 直接定义在 handler 和 task 的公共底层. 注入即可, 就没有这么多麻烦事了. |
5
sthwrong 23 天前 ![]() task 没有 sshclient 的依赖,依赖是在 handler 中,task 只需要提供参数给 handler 就行了,task 依赖一个工厂函数或者工厂接口就行。
|
6
NessajCN 23 天前 ![]() @chaleaochexist 你直接发代码吧,我怀疑你提了个 xy 问题,
也许你的原始需求有更直接简单的解决法而不用注入来注入去 |
7
iseki 23 天前 via Android ![]() 要不要在这里叠加 factory 看你的倾向和你使用的 di 框架的能力啊。有的时候工厂是个很简单的解决方案,特别是你想更精确地控制生命周期时。
|
8
iseki 23 天前 via Android ![]() 我虽然不是很确信你的业务,不过如果按照我对你业务的猜测,这里使用 factory pattern 是正确的。
|
9
Sendya 23 天前 ![]() |
10
kfpenn 23 天前 ![]() 我也有这种嵌套的依赖,最后是用 interface 解决的
|
![]() |
11
darksword21 PRO ![]() op 最好能给出现在的伪代码或者哪个循环引用的伪代码
|
![]() |
12
everhythm 23 天前 ![]() lz 不如画图说明下设想的流程和调用逻辑,这里 handler 和 task 名称和职责感觉不清楚
假设是 handler 生成 task ,task 调用 sshclient 干活 1. handler 没有直接管 sshclient 2. 没有看懂 |
13
bli22ard 23 天前 ![]() type CommandRunner interface {
exec(cmd string) (ret string, err error) } |
![]() |
14
chaleaochexist OP @everhythm
@darksword21 @Sendya @iseki @NessajCN @sthwrong @sunny352787 @Dorathea @NessajCN 谢谢你们的回复, 我写了一个 伪代码, 希望可以把问题解释清楚. https://github.com/chaleaoch/golang_demo.git 一共是三个版本 v1, v2, v3 我把问题都放到了注释中, 可以搜索关键字 "问题" . 各个版本之间的主要差别, 可以搜索关键字 "修改" 如果我得代码有其他的地方的写法上的问题, 随意批评指导, 谢谢你们. 谢谢!! |
15
NessajCN 22 天前
@chaleaochexist 你完全搞错 interface 的用法了,
go 的 interface 跟 jvav 的不是一回事, 你定义了一大堆完全永不上的 interface go 的 interface 可以类比 python 的 protocol 或 rust 的 trait, 是为了方便你写「非特定类型参数」的函数,或者早期的泛型来用的。 具体的用法是你定义一个 struct 和 interface , 并给 struct 实现 interface 里的函数, 之后你定义的参数为该 interface 的函数就可以直接传这个 struct 的实例了 我的建议还是如 3# 时候讲的,你初始化就初始化 client 然后传引用进 task 函数就好,初始化时候也根本不需要知道 ip type SSHClient struct {} func (s *SSHClient) ExecuteCommand(user string, pass string, host string, cmd string) { // blablabla } // 初始化 sshclient 放到 main 函数里, 然后传给下面的 task func (q *DemoTask) Run(sshClient *SSHClient) { mIps, _ := q.mIPRepo.GetByType("exampleType") // 这一条也可以大幅简化 for _, mIp := range mIps { out, _ := sshClient.ExecuteCommand(mIp.Username, mIp.Password, mIp.Ip+":"+mIp.Port, "exampleCommand") fmt.Println(out) } } |
![]() |
16
chaleaochexist OP @NessajCN 像你这么写, 最大的问题是如何 mock?
无论是 GetByType 是查询数据库的, 如果不做成接口, 如何 mock ssh 也是, 在单元测试的时候, 我不希望真的去 ssh 执行一条命令. |
17
NessajCN 22 天前
@chaleaochexist 就像我上一个回复内容提到的,你先要弄明白 go 的 interface 究竟是啥。
先把 jvav 思维彻底舍弃才好跟你讲下一步 |
![]() |
18
chaleaochexist OP 无论是 --> ~~无论是~~
|
![]() |
19
chaleaochexist OP @NessajCN 好吧...其实我根本就不会 java...
golang 如果不通过 interface 没法 mock. 或者 sshClient 这个接口 可以有多个实现. 我希望注入而不是将 NewsshClient1 改成 NewsshClient2 |
20
NessajCN 22 天前
@chaleaochexist 都说了接口不是这么用.....
sshClient 显然是个实例,咋会是接口呢。 go 的 interface 是在定义函数时候作为「类泛型」传参用的,你的 sshClient 必然是要初始化然后调方法的, 概念风马牛不相及呀 |
![]() |
21
chaleaochexist OP @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 之后, 在哪里 定义接口的问题. |
![]() |
22
chaleaochexist OP @NessajCN 不过还是谢谢你耐心和我讨论问题... 希望你能和我继续 讨论.
|
![]() |
23
chaleaochexist OP 实际上我第一版 --> 在我提供的 github 仓库中没有体现, 是我之前在工作中的第一版.
|
![]() |
24
chaleaochexist OP @NessajCN 别着急大佬 等我再写一个版本 和你一起讨论.
|
![]() |
25
chaleaochexist OP @NessajCN 我猜 https://github.com/chaleaoch/golang_demo/tree/master/v4
这个文件夹下的代码 比较符合你的品味吧? 但是我得问题是, 如果我想注入 handler? 要如何做? 答案是不是: handler 就不应该注入 而是直接调用? 实际上我也是这么做的, 但是我需要你们 确认一下!!! 这回 我说明白了吗? |
26
NessajCN 22 天前
@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 {} |
![]() |
27
chaleaochexist OP @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 是否符合逻辑呢? |
28
NessajCN 22 天前 ![]() @chaleaochexist
合逻辑呀 虽然要我来 code review 的话 SSHClient 这些接口定义都不需要只留 StandardSSH 就行了 不过你说除了 StandardSSH 之外还有其他 struct 需要传给 CmdHandler 那确实可以用 interface 这样的话你需要把 Connect(username string, password string, host string) error 加到 SSHClient 的接口定义里 |
![]() |
29
chaleaochexist OP @NessajCN 你平时肯定不做单元测试. 我确定.
|
30
NessajCN 22 天前
@chaleaochexist
SSHClient 是 struct 就不能写脱机测试了又是咋得出的结论呀... |
![]() |
31
chaleaochexist OP @NessajCN 没办法模拟, 反正我不会...
|
32
NessajCN 22 天前
@chaleaochexist
写测试更用不到 interface 了 哪怕你代码里用 interface 作为参数定义函数 实际测试里也肯定是传的实现了那个接口的 struct 不理解你因为需要单元测验而必须定义 interface 的原理 |
![]() |
33
chaleaochexist OP @NessajCN 你写个测试就知道了.
譬如 repo 访问数据库吧 现在需要你 在没有数据库的情况下测试 service 层的函数 如何 mock 假数据. 只有接口可以做到. 你传入一个结构体 结构体的依赖是 db.DB 他可是真的通过 TCP 去连数据库. 但是如果是接口 我就可以做一个假的 struct 去实现这个接口, 然后返回假数据就行了. 总之一句话 你尝试给你的 repo 层 写单元测试就明白了. |
34
sthwrong 22 天前
依赖就分错了,provider 依赖 repo , 提供方法返回 clients ,handler 依赖 clients ,提供方法根据传入 clients 和 cmd 构建单个或者多个 handler ,task 依赖 handler ,提供方法执行 handler ,每个实现自己声明一个接口 。依赖清晰了,每一层都可以在 test 中声明新的 mock 实例实现 mock 方法替代调用。
|
35
NessajCN 22 天前
@chaleaochexist
fakeData := DbData{} FunctionToBeTested(fakeData) 这里头有接口啥事啊..... 你的意思是 DbData{} 这个数据结构体初始化的时候必须连数据库? |
36
sthwrong 22 天前
|
![]() |
37
chaleaochexist OP @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 连接, 传一个假的 编译失败. |
![]() |
38
chaleaochexist OP |
39
NessajCN 22 天前 via Android
|
40
sthwrong 22 天前 ![]() @chaleaochexist #38 动态数据本身就是来自 repo 啊,生成动态结果就依赖 repo 给数据,明显的依赖关系。依赖层级对了,每层都可以决定是否需要 interface ,想 mock 的都可以,调用层依赖这个 interface ,test 的时候自己实现个 mock 实例传进去。
|
41
sthwrong 22 天前 ![]() @chaleaochexist #38 就算不是来自 repo ,如果来自其他模块的方法,让 provider 依赖这个模块的方法,如果需要方法中的客户端数据可以被 mock ,就给这给方法的实例定义一个接口,test 的时候传一个自定义的实现了方法的实例。我的 main 方法直接调用的原始实现,test 里面大多数都给了 mock 实现,所以打印的结果才又 mock 信息。
![]() |
![]() |
42
chaleaochexist OP @NessajCN 我没话讲了大佬, 后端 这么做不是太正常了吗?
一个 service 有多少个 io 操作啊 这都是最简单的了. |
![]() |
43
chaleaochexist OP @sthwrong 虽然你说的有瑕疵, 但是给我提供了新思路,我明天试一下. 谢谢佬友!!!
|
![]() |
44
chaleaochexist OP @sthwrong #34
"依赖就分错了,provider 依赖 repo , 提供方法返回 clients ,handler 依赖 clients ,提供方法根据传入 clients 和 cmd 构建单个或者多个 handler ,task 依赖 handler ,提供方法执行 handler ,每个实现自己声明一个接口 。依赖清晰了,每一层都可以在 test 中声明新的 mock 实例实现 mock 方法替代调用。" 提供方法返回 clients 中的 clients 是接口还是结构体, 接口的话定义在哪里? 针对这个问题 我又 发了个帖子 https://v2ex.com/t/1152334#reply0 大佬感兴趣的话, 点拨一二.. |