0%

golang抽取接口,依赖注入(依赖倒置)解决包引用关系

本文记录了作者在golang开发中,通过抽取接口,依赖注入的方式,解决包与包之间的不合理引用关系。

总结来说:

面向接口编程,并且golang中接口函数的参数最好是标准库的类型

场景

目前项目中有一个业务逻辑包business_logic,两个工具库包pkg1pkg2,其中

  • pkg1是旧库,API不宜改动,pkg2是新库,尚未正式使用
  • business_logic会使用pkg1pkg2
  • pkg1内部要添加使用pkg2的逻辑
1
2
3
4
5
6
7
8
// pkg1/main.go
package pkg1

import "pkg2"

func ExternalAPI() {
pkg2.ExternalAPI(pkg2.S{})
}
1
2
3
4
5
6
7
8
9
// pkg2/main.go
package pkg2

type S struct {
param1 int
}

func ExternalAPI(s S) {
}
1
2
3
4
5
6
7
8
9
10
11
12
// business_logic/main.go
package main

import (
"pkg1"
"pkg2"
)

func main() {
pkg1.ExternalAPI()
pkg2.ExternalAPI(pkg2.S{})
}

这样就引起了一个问题:

business_logic其实引用了两次pkg2,一次是直接引用,一次是通过pkg1间接引用,将来在版本更迭中,很有可能会出现直接引用的版本和间接引用的版本不一致的情况,从而引起未知bug

解决尝试

如果不希望两次引用,那么最好的方式是消除pkg1pkg2的引用,消除引用的方式是

  • pkg1抽象出一个接口,
  • pkg2提供结构体,实现pkg1抽象出的接口

这样,pkg2实际上就变成了pkg1的一个插件,只要在business_logic初始化的时候,将pkg2的插件注入到pkg1里去就行

但是这样的尝试失败了,我们先来看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// pkg1/main.go
package pkg1

import "pkg2"

type Plugin interface {
ExternalAPI(s pkg2.S)
}

var plugin Plugin

func ExternalAPI() {
if plugin != nil {
plugin.ExternalAPI(pkg2.S{})
}
}

func SetPlugin(p Plugin) {
plugin = p
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pkg2/main.go
package pkg2

type S struct {
param1 int
}

type Plugin struct {
}

func (p *Plugin) ExternalAPI(s S) {
}

func ExternalAPI(s S) {
p := Plugin{}
p.ExternalAPI(s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// business_logic/main.go
package main

import (
"pkg1"
"pkg2"
)

func main() {
pkg1.SetPlugin(&pkg2.Plugin{})
pkg1.ExternalAPI()
pkg2.ExternalAPI(pkg2.S{})
}

我们发现,pkg1pkg2的引用仍旧存在,其原因在于抽取出来的接口函数中的参数是属于pkg2

1
2
3
type Plugin interface {
ExternalAPI(s pkg2.S)
}

最终解决方案

由于pkg2是新库,所以我们决定更改它的接口,最终的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// pkg1/main.go
package pkg1

type Plugin interface {
ExternalAPI(param int)
}

var plugin Plugin

func ExternalAPI() {
if plugin != nil {
plugin.ExternalAPI(0)
}
}

func SetPlugin(p Plugin) {
plugin = p
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// pkg2/main.go
package pkg2

type Plugin struct {
}

func (p *Plugin) ExternalAPI(s int) {
}

func ExternalAPI(s int) {
p := Plugin{}
p.ExternalAPI(s)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// business_logic/main.go
package main

import (
"pkg1"
"pkg2"
)

func main() {
pkg1.SetPlugin(&pkg2.Plugin{})
pkg1.ExternalAPI()
pkg2.ExternalAPI(0)
}

可以看到,这回彻底解决了pkg1引用pkg2的问题,代价就是将pkg2.S这个结构体参数展开了

视具体业务情况而定,我们可以通过:

  1. 展开结构体
  2. 将结构体换做map[string]interface{}(当然需要手动做字段的提取和塞入)
  3. 将结构体换做string,用JSON传参(手动Marshal和Unmarshal)
  4. 将参数类型放到新的第三方库pkg3中(这样就又要维护引用的pkg3版本一致)

软件开发中没有silver-bullet,只有trade-off,这次的方案,也还算满意