关于依赖注入(DI)
依赖注入和控制反转在 Java 里应该很常见了,不过 Go 里并不那么流行。
- 为什么需要 DI
- 为什么需要 IoC
Go 的很大一个特点就是简单,Less is more 深入人心。我也是因为这个原因从 Java 转向了 Go。用 Go 写项目没有那么多条条框框,比如一个简单的 web 项目结构可能是这样的。
- config
- handler
- service
- - user/
- - biz1/
- - biz2/
- model
- - user/
- - biz1/
- - biz2/
- router.go
- main.go
然后在 main.go 里执行所有包的初始化,比如
config.Init()
model.Init()
biz1.Init()
使用其它的模块就通过包名调用
config.GetHost()
user.UpdateAvater(userid, avatarUrl)
对于小项目这样一般不会有问题,写起来也很快。
但是随着业务变的复杂,就容易出现一些新的问题。
一个问题是循环依赖。比如 service 下面有两个业务模块包 biz1 和 biz2,两个包可能在需要相互调用,这样的话编译时就会报错。
第二个是依赖关系混乱,在 biz2 里直接调用 biz1 包里的方法,这种依赖关系是隐式的,只有从 import 里能看到谁依赖了谁。因为依赖关系不明显,在初始化各个包时也很难明确顺序。
还有一个使用包级别的结构产生的另外一个问题是都是单例模式,如果有一个 biz2 依赖 biz1,但是 biz3 也依赖 biz1,但是它需要创建两个不同的实例
解决循环依赖的方式有两种,一种是将包分拆成多个小包。一种是不再依赖包,而改用接口,调用方依赖接口。
要解决依赖关系混乱,最好的方式是明确依赖。一般是在初始化包时将依赖作为参数传递进来,如果依赖项是一个包,那无法这样做。所以还是需要改成依赖结构体或者接口的形式。
所以最终的形式便成了所有的服务以结构体的形式存在,而不是包。然后依赖项作为这个结构体的字段,在初始化结构体时传入所需要的依赖项。
然后在项目初始化时,根据每个服务结构体需要的参数就可以知道它的依赖项,可以很好的按顺序进行初始化。可能大部分可以使用单例模式就行了,小部分需要创建两个不同依赖实例,分别再初始化不同的被依赖结构。
另外因为 go 的接口和实现是弱关联的,可以先不使用接口,直接依赖结构体,等有需要了,再抽象成接口。
这一整套改造完就是依赖注入了,依赖注入按我的理解就是如果 A 依赖 B,不应该让 A 去初始化 B。而是应该让第三方将 B 注入给 A,然后 A 再使用。A 不应该关心 B 的生命周期。
这里的 A 依赖的 B 可以是结构体,也可以是接口。但是更好的是接口,这样 B 后面更改实现。
根据上面的方式,我们在初始化 A 时会要求传入一个 B 的接口。这里有一个问题是这个 B 的接口应该是在 B 里定义还是在 A 里定义。我之前一直是在 B 里定义的,定义 B 的接口,同时实现 B。在 B 里面定义逻辑关系更近,但是假如 B 被多方依赖就比较不方便了。而且也在形式上让 A 和 B 产生了关联。最好的方式是 A 的依赖接口在 A 里定义,并且保持最小原则,只定义自己需要的接口。