Go 语言的依赖注入 已翻译 100%

oschina 投递于 2014/07/29 08:20 (共 8 段, 翻译完成于 09-07)
阅读 9807
收藏 98
Go
7
加载中

依赖注入(DI)是一种解耦组件之间依赖关系的设计模式。在需要的时候,不同组件之间可以通过一个统一的界面获取其它组件中的对象和状态。Go语言的接口设计,避免了很多需要使用第三方依赖注入框架的情况(比如Java,等等)。我们的注入方案只提供非常少的类似Dager或Guice中的注入方案,而专注于尽量避免手动去配置对象和组件之间的依赖关系。因为,我们认为如果在Go代码库中,注入能够更加容易理解,就根本没有必要那样。

在Go中实现注入只需要这几个简单的步骤:

yanchao90
翻译于 2014/08/31 21:21
1

全局变量

先从一个一致的、崇高的目标开始,我们需要一些如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。

虽然在创建全局变量的时候共享资源让它们(至少)有两个缺点:
首先,因为组件的依赖关系不明确,所以代码是很难写的;
其次,你很难去测试你写的代码,在并行条件下更是几乎不可能。

尽管测试是非常快的(我们希望确保一直很快),但是能够在并行环境下测试才是最重要的。使用全局连接对象时,后台服务无法在并发条件下测试出相同的数据。

yanchao90
翻译于 2014/08/31 22:59
1

清除全局变量

为了清除全局变量,我们先从一个通用模式开始。我们的组件现在显示依赖,我们将,一个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
翻译于 2014/07/29 08:52
1

新的问题

真棒!在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()函数中共用这些代码。所以测试代码也变得越来越繁杂、冗余,却还是经常找不出实际问题。简而言之,我们解决了一个问题,却产生了另一种问题。

yanchao90
翻译于 2014/09/03 18:56
1

标识 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
翻译于 2014/09/06 11:26
1
注入库是这项工作的成果和我们的解决方案。它使用结构标签(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 字段上也有注入标签。

微笑的书生
翻译于 2014/08/22 10:17
2

我们的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
翻译于 2014/09/06 12:00
1

胜利啦

我们的main()函数更易管控了。现在,手动新建一个仅有两个case的实例:如果实例需要在main中得到配置信息,或者如果其需要请求一个接口类型。即使如此,我们往往新建一些不完整的实例,让依赖注入为我们补充完整。测试代码大幅度的精简,并且现在可以在不需要知道对象图表的情况下为测试提供执行。这使得测试更具弹性,可以改变相当大。重构同样变得简单起来,就像抽出逻辑而不需要手动调整我们在各类main()中新建的对象图表。

总体来说,我们对结果和自从介绍了依赖注入,我们的代码库的演化感到非常高兴。

资源

你可以在Github上找到该库的资源:

https://github.com/facebookgo/inject

我们同时提供文档,尽管最好的学习方式是实际“玩”一下:

https://godoc.org/github.com/facebookgo/inject

我们非常喜爱能得到贡献,所以在贡献时请确保下面的测试可以通过:

https://travis-ci.org/facebookgo/inject

0x0bject
翻译于 2014/07/29 08:48
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(16)

liudng
liudng
见过有个老师讲go的时候, 变量起名为this, 感觉有点误导思维
诺晨
诺晨
喜欢GO的原因,就是因为它的简洁!请不要把其它语言的特性硬生生的往上面套!
asdfsx
asdfsx
从依赖注入感觉到了一股浓浓的java味道
asdfsx
asdfsx

引用来自“中山野鬼”的评论

不知道是翻译的原因,还是作者的原因,怎么写的这么幼稚啊。哈。一个不变的哲理,你描述的具体形式越少,你描述的内涵越少。简单说,大量的配置,无论什么语言,在于层级过于扁平化,即便c语言,主程序的大量配置工作也不可避免,当然更多的解决方案是传递给不同的子程序去做,而子程序作为独立模块进行测试和确认。即便这样,也是不能降低所谓的配置的工作量,但对一些相对固化的模块配置而言,可以降低重复工作量。如果编译器可以帮你组合配置各种多样性的方案,那么回过头来,组合出的各种方案,细微的差异,也会在组合名单中体现。。。哈。慢慢查吧。
最后喷一句,文章中所谓的精简了main函数,和所谓的c语言的宏替换有何差异?何做个子函数实现又有何差异。哈。
这样看起来比较牛x,比较容易骗经验
傅小黑
傅小黑
好乱的感觉
喻恒春
喻恒春
这还是显示构造对象字段啊, 然后 reflect.New 完成的, 这方法完全是多余的啊, 既然显示构造, 那还不如直接硬编码对字段赋值.
这方法怎么看都没有 Martini 的 Injector 通过 reflect.Type 进行类型匹配来的安逸.
中山野鬼
中山野鬼
不知道是翻译的原因,还是作者的原因,怎么写的这么幼稚啊。哈。一个不变的哲理,你描述的具体形式越少,你描述的内涵越少。简单说,大量的配置,无论什么语言,在于层级过于扁平化,即便c语言,主程序的大量配置工作也不可避免,当然更多的解决方案是传递给不同的子程序去做,而子程序作为独立模块进行测试和确认。即便这样,也是不能降低所谓的配置的工作量,但对一些相对固化的模块配置而言,可以降低重复工作量。如果编译器可以帮你组合配置各种多样性的方案,那么回过头来,组合出的各种方案,细微的差异,也会在组合名单中体现。。。哈。慢慢查吧。
最后喷一句,文章中所谓的精简了main函数,和所谓的c语言的宏替换有何差异?何做个子函数实现又有何差异。哈。
wendal
wendal

引用来自“fotomxq”的评论

最近GO语言的文章很多啊,唯一的问题是编译器用起来很难受,其他看起来还好。不知道国内有人用没?

引用来自“chenwenli”的评论

国内比国外火。有大小公司都在用。 请百度下golang中文社区,找个群了解情况

引用来自“lesou”的评论

为什么国内火啊。要是国外也火这样就是真的火。有前途

引用来自“se77en”的评论

谁说国外不火的?你上twitter看看就知道国外火不火了
上twitter如何看火不火? 求教.
se77en
se77en

引用来自“fotomxq”的评论

最近GO语言的文章很多啊,唯一的问题是编译器用起来很难受,其他看起来还好。不知道国内有人用没?

引用来自“chenwenli”的评论

国内比国外火。有大小公司都在用。 请百度下golang中文社区,找个群了解情况

引用来自“lesou”的评论

为什么国内火啊。要是国外也火这样就是真的火。有前途
谁说国外不火的?你上twitter看看就知道国外火不火了
lesou
lesou

引用来自“fotomxq”的评论

最近GO语言的文章很多啊,唯一的问题是编译器用起来很难受,其他看起来还好。不知道国内有人用没?

引用来自“chenwenli”的评论

国内比国外火。有大小公司都在用。 请百度下golang中文社区,找个群了解情况
为什么国内火啊。要是国外也火这样就是真的火。有前途
返回顶部
顶部