本文首发于我的个人博客
本文记录了作者在golang开发中,通过抽取接口,依赖注入的方式,解决包与包之间的不合理引用关系。
总结来说:
面向接口编程,并且golang中接口函数的参数最好是标准库的类型
场景
目前项目中有一个业务逻辑包business_logic
,两个工具库包pkg1
和pkg2
,其中
pkg1
是旧库,API不宜改动,pkg2
是新库,尚未正式使用business_logic
会使用pkg1
和pkg2
pkg1
内部要添加使用pkg2
的逻辑
// pkg1/main.go
package pkg1
import "pkg2"
func ExternalAPI() {
pkg2.ExternalAPI(pkg2.S{})
}
// pkg2/main.go
package pkg2
type S struct {
param1 int
}
func ExternalAPI(s S) {
}
// business_logic/main.go
package main
import (
"pkg1"
"pkg2"
)
func main() {
pkg1.ExternalAPI()
pkg2.ExternalAPI(pkg2.S{})
}
这样就引起了一个问题:
business_logic
其实引用了两次pkg2
,一次是直接引用,一次是通过pkg1
间接引用,将来在版本更迭中,很有可能会出现直接引用的版本和间接引用的版本不一致的情况,从而引起未知bug
解决尝试
如果不希望两次引用,那么最好的方式是消除pkg1
对pkg2
的引用,消除引用的方式是
pkg1
抽象出一个接口,- 让
pkg2
提供结构体,实现pkg1
抽象出的接口
这样,pkg2
实际上就变成了pkg1
的一个插件,只要在business_logic
初始化的时候,将pkg2
的插件注入到pkg1
里去就行
但是这样的尝试失败了,我们先来看一下代码
// 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
}
// 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)
}
// business_logic/main.go
package main
import (
"pkg1"
"pkg2"
)
func main() {
pkg1.SetPlugin(&pkg2.Plugin{})
pkg1.ExternalAPI()
pkg2.ExternalAPI(pkg2.S{})
}
我们发现,pkg1
对pkg2
的引用仍旧存在,其原因在于抽取出来的接口函数中的参数是属于pkg2
的
type Plugin interface {
ExternalAPI(s pkg2.S)
}
最终解决方案
由于pkg2
是新库,所以我们决定更改它的接口,最终的代码如下
// 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
}
// pkg2/main.go
package pkg2
type Plugin struct {
}
func (p *Plugin) ExternalAPI(s int) {
}
func ExternalAPI(s int) {
p := Plugin{}
p.ExternalAPI(s)
}
// business_logic/main.go
package main
import (
"pkg1"
"pkg2"
)
func main() {
pkg1.SetPlugin(&pkg2.Plugin{})
pkg1.ExternalAPI()
pkg2.ExternalAPI(0)
}
可以看到,这回彻底解决了pkg1
引用pkg2
的问题,代价就是将pkg2.S
这个结构体参数展开了
视具体业务情况而定,我们可以通过:
- 展开结构体
- 将结构体换做
map[string]interface{}
(当然需要手动做字段的提取和塞入) - 将结构体换做
string
,用JSON传参(手动Marshal和Unmarshal) - 将参数类型放到新的第三方库
pkg3
中(这样就又要维护引用的pkg3
版本一致)
软件开发中没有silver-bullet,只有trade-off,这次的方案,也还算满意