依赖注入(DI)是一种解耦组件之间依赖关系的设计模式。在需要的时候,不同组件之间可以通过一个统一的界面获取其它组件中的对象和状态。Go语言的接口设计,避免了很多需要使用第三方依赖注入框架的情况(比如Java,等等)。我们的注入方案只提供非常少的类似Dager或Guice中的注入方案,而专注于尽量避免手动去配置对象和组件之间的依赖关系。因为,我们认为如果在Go代码库中,注入能够更加容易理解,就根本没有必要那样。
在Go中实现注入只需要这几个简单的步骤:
全局变量先从一个一致的、崇高的目标开始,我们需要一些如Mongo、Memcache等服务的全局连接对象。大致是这样的:
var MongoService mongo.Service
func InitMongoService(url string) {
MongoService = ...
}
func GetApp(id uint64) *App {
a := new(App)
MongoService.Session().Find(..).One(a)
return a
}
通常 main() 函数会调用配置在flags或configuration文件中如 InitMongoService 这样的各种初始化函数。这时,像 GetApp 这样的函数就可以使用这些服务和连接了。当然,有时候我们会忘记初始化全局变量,被 nil 引发panic。
虽然在创建全局变量的时候共享资源让它们(至少)有两个缺点:
首先,因为组件的依赖关系不明确,所以代码是很难写的;
其次,你很难去测试你写的代码,在并行条件下更是几乎不可能。
尽管测试是非常快的(我们希望确保一直很快),但是能够在并行环境下测试才是最重要的。使用全局连接对象时,后台服务无法在并发条件下测试出相同的数据。
清除全局变量为了清除全局变量,我们先从一个通用模式开始。我们的组件现在显示依赖,我们将,一个Mongo服务,或者一个缓存服务。大致来讲,我们上面那个幼稚的例子现在看起来应当是这样的:
type AppLoader struct {
MongoService mongo.Service
}
func (l *AppLoader) Get(id uint64) *App {
a := new(App)
l.MongoService.Session().Find(..).One(a)
return a
}
许多引用全局变量的函数现在变成了结构体中存储了它们的依赖。
新的问题真棒!在main()方法中,我们用一系列的构造代替了全局变量和函数,解决了我们之前遇到的问题。但是... 一看main()函数就知道了,太杂乱无章了。
一开始就这么乱了:
func main() {
mongoURL := flag.String(...)
mongoService := mongo.NewService(mongoURL)
cacheService := cache.NewService(...)
appLoader := &AppLoader{
MongoService: mongoService,
}
handlerOne := &HandlerOne{
AppLoader: appLoader,
}
handlerTwo := &HandlerTwo{
AppLoader: appLoader,
CacheService: cacheService,
}
rootHandler := &RootHandler{
HandlerOne: handlerOne,
HandlerTwo: handlerTwo,
}
...
}
如果一直这样写下去,main()函数的方法体将会被被大量的代码占据。而这些代码仅仅只是做了两件很普通的事情:分配内存空间、装配对象和组件关系。如果我们有非常多的二进制代码和库需要引用,我们就需要一遍又一遍的写这些无聊的代码。这里特别需要注意的是,不要被nil引发panic。比如我们忘记把CacheService传递给HandlerTwo,然后就引发了一个运行时panic。我们试图构造一个方法,但是却变得有些失控。还需要写一大堆的代码手动检查nil。因为必须手动装配对象并确保运行正常,我们的开发对此非常恼火。测试人员甚至还需要自己装配对象、构建关系,显然他们不会在main()函数中共用这些代码。所以测试代码也变得越来越繁杂、冗余,却还是经常找不出实际问题。简而言之,我们解决了一个问题,却产生了另一种问题。