全局变量先从一个一致的、崇高的目标开始,我们需要一些如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 } 许多引用全局变量的函数现在变成了结构体中存储了它们的依赖。 |
0x0bject
|
新的问题真棒!在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()函数中共用这些代码。所以测试代码也变得越来越繁杂、冗余,却还是经常找不出实际问题。简而言之,我们解决了一个问题,却产生了另一种问题。 |
晏雨涵
|
标识 Mundane
我们中的一些人对DI系统比较有经验,并且我们都不认为这仅仅是纯娱乐性的经验。因此,当我们第一次讨论用 DI系统解决这个新问题时,就已经有大量的push back(我理解为经验储备...高手求解)。 根据这些规则,当我们需要一些东西的时候,我们决定需要确保避免已知的复杂性并制定了一些基本准则: 1. 没有代码生成。我们的开发编译步骤仅仅用 go install,我们不想引入额外的步骤。与这条规则相关的是无文件扫描,我们不想把项目变成一个O(大量文件)系统,同时也要防止增加编译时间。 2. 没有子图。子图的概念是以每个请求为基准(a per-request basis)允许注入发生,简单来说,一个子图必须能够彻底地区分"global"生命周期和"per-request"(每个请求)生命周期的对象,并且确保在所有请求中不混淆这些"per-request"对象。我们决定仅仅允许"global"生命周期对象的注入,因为这正是我们现在面临的问题。 3. 避免代码执行。DI本质上使代码很难理解,我们想避免定制化的代码执行/钩子,使它更容易理解。 根据这些准则,我们的目标变得比较清晰了: 1. 注入应该分配对象。 2. 注入应该将对象图连接起来。 3. 注入应该在程序启动时仅仅运行一次。 我们也讨论了supporting constructor(支持构造函数)功能,但现在避免对他们增加支持。 |
xiaoaiwhc1
|
注入库是这项工作的成果和我们的解决方案。它使用结构标签(struct tags)来实现注入功能,可为具体的类型注入,也支持对接口类型注入,只要明确接口类型的具体类型,它还有些不太常用的功能,比如按名称注入(named injection)。前面的简单示例现在看起来是这样:
type AppLoader struct { MongoService mongo.Service `inject:""` } func (l *AppLoader) Get(id uint64) *App { a := new(App) l.MongoService.Session().Find(..).One(a) return a } 没有任何改变,除了在 MongoService 字段上增加了注入标签。有几种不同的方式使用注入标签,但这是最常见用法,它简洁地表明了期望注入一个 mongo.Service 实例。同样地,可以想象 HandlerOne,HandlerTwo 和 RootHandler 字段上也有注入标签。 |
张露兵
|
我们的main()现在看起来这样: func main() { mongoURL := flag.String(...) mongoService := mongo.NewService(mongoURL) cacheService := cache.NewService(...) var app RootHandler err := inject.Populate(mongoService, cacheService, &app) if err != nil { panic(err) } ... } 更短!注入的整个流程大概是这样: 1. 查看每个已经提供的实例,最终遇到RootHandler类型的app实例. 2. 查看RootHandler字段,寻找带 inject 标签的*HandlerOne,发现没有*HandlerOne实例存在,于是就创建一个并将它赋值给这个字段. 3. 对刚刚创建的HandlerOne实例继续进行与步骤2类似的查找,找到AppLoader字段,简单地创建它. 4. 对于AppLoader实例,它需要一个mongo.Service实例,它发现当我们调用Populate时已经创建过一个实例,于是它将那个实例赋值到这里. 5. 当它对HandlerTwo进行同样的查找时,它使用已经创建的AppLoader实例,因此这两个Handlers共享这个AppLoader实例. 注入分配对象并为我们将graph连接起来。调用Populate后,注入不再做任何事情,剩下的跟之前没有注入时的行为都一样了. |
xiaoaiwhc1
|
胜利啦我们的main()函数更易管控了。现在,手动新建一个仅有两个case的实例:如果实例需要在main中得到配置信息,或者如果其需要请求一个接口类型。即使如此,我们往往新建一些不完整的实例,让依赖注入为我们补充完整。测试代码大幅度的精简,并且现在可以在不需要知道对象图表的情况下为测试提供执行。这使得测试更具弹性,可以改变相当大。重构同样变得简单起来,就像抽出逻辑而不需要手动调整我们在各类main()中新建的对象图表。 总体来说,我们对结果和自从介绍了依赖注入,我们的代码库的演化感到非常高兴。 资源你可以在Github上找到该库的资源: https://github.com/facebookgo/inject 我们同时提供文档,尽管最好的学习方式是实际“玩”一下: https://godoc.org/github.com/facebookgo/inject 我们非常喜爱能得到贡献,所以在贡献时请确保下面的测试可以通过: |
0x0bject
|
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
有疑问加站长微信联系(非本文作者)