在现代的 web
框架里面,基本都有实现了依赖注入的功能,可以让我们很方便地对应用的依赖进行管理,同时免去在各个地方
new 对象的麻烦。比如 Laravel
里面的
Application
,又或者 Java 的 Spring
框架也自带依赖注入功能。
今天我们来看看 go 里面实现依赖注入的一种方式,以 inject
库为例子(https://github.com/flamego/flamego/tree/main/inject)。
我们要了解一个软件的设计,先要看它定义了一个什么样的模型,但是在了解模型之前,我们更应该清楚了解,为什么会出现这个模型,也就是我们构建出了这个模型到底是为了解决什么问题。
依赖注入要解决的问题
我们先来看看,在没有依赖注入之前,我们需要的依赖是如何构建出来的,假设有如下
struct
定义:
1 | type A struct { |
假设我们要调用 test
,就需要创建一个 C
的实例,而创建 C
的实例需要创建一个 B
的实例,而创建 B
的实例需要一个 A
的实例。如下是一个例子:
1 | a := A{} |
我们可以看到,这个过程非常的繁琐,只有一个地方需要这样调用
test
还好,如果有多个地方都需要调用
test
,那我们就要做很多创建实例的操作,而且一旦实例的构建过程发生变化,我们就需要改动很多地方。
所以现在的 web 框架里面一般都将这个实例化的过程固化下来,在框架的某个地方注册一些实例化的函数,在我们需要的时候就调用之前注册的实例化的函数,实例化之后,再根据需要看看是否需要将这个实例保留在内存里面,从而在免去了手动实例化的过程之外,节省我们资源的开销(不用每次使用的时候都实例化一次)。
而这里说到的固化的实例化过程,其实就是我们本文所说的依赖注入。在
Laravel
里面我们可以通过 ServiceProvider
的
app()->register()
或者 app()->bind()
等函数来做依赖注入的一些操作。
inject 依赖注入模型/设计
以下是 Injector
的大概模型,Injector
接口里面嵌套了
Applicator
、Invoker
、TypeMapper
接口,之所以这样做是出于接口隔离原则考虑,因为这三者代表了细化的三种不同功能,分离出不同的接口可以让我们的代码更加的清晰,也会更利于代码的后续演进。
Injector
:依赖注入容器Applicator
:结构体注入的接口Invoker
:使用注入的依赖来调用函数TypeMapper
:类型映射,需要特别注意的是,在Injector
里面,是通过类型来绑定依赖(不同于Laravel
的依赖注入容器可以通过字符串命名的方式来绑定依赖,当然将Injector
稍微改改也是可以实现的,就看有没有这种需求罢了)。
1 | // 依赖注入容器 |
表示成图像大概如下:
我们可以通过 Injector
的 TypeMapper
来往依赖注入容器里面注入依赖,然后在我们需要为结构体的字段注入依赖,又或者为函数参数注入依赖的时候,可以通过
Applicator
或者 Invoker
来实现注入依赖。
而 SetParent
这个方法比较有意思,它其实将
Injector
这个模型拓展了,形成了一个有父子关系的模型。在其他语言里面可能作用不是很明显,但是在
go 里面,这个父子模型恰好和 go 的协程的父子模型一致。在 go
里面,我们可以在一个协程里面再创建一个
Injector
,然后在这里面定义一些在当前协程以及当前协程子协程可以用到的一些依赖,而不用影响外部的
Injector
。
当然上面说到的协程只是 Injector
里面
SetParent
的一种用法,另外一种用法是,我们的 web
应用往往会根据路由前缀来划分为不同的组,而这种路由组的结构组织方式其实也是一种父子结构,在这种场景下,我们就可以针对全局注入一些依赖的情况下,再针对某个路由组来注入路由组特定的依赖。
injector 的依赖注入实现
我们来看看 injector
的结构体:
1 | type injector struct { |
这个结构体定义很简单,就只有两个字段,values
和
parent
,我们通过 TypeMapper
注入的依赖都保存在
values
里面,values
是通过反射来记录我们注入的参数类型和值的。
那我们是如何注入依赖的呢?再来看看 TypeMapper
的
Map
方法:
1 | func (inj *injector) Map(values ...interface{}) TypeMapper { |
我们可以看到,对于传入给 Map
的参数,这里获取了它的反射类型作为 values
map 的
key,而获取了传入参数的反射值作为 values
里面 map
的值。其他的两个方法 MapTo
、Set
也是类似的功能,最终的效果都是获取依赖的类型作为 values 的
key,依赖的值作为 values 的值。
到此为止,我们知道 Injector
是如何注入依赖的了。
那么它又是如何去从依赖注入容器里面拿到我们注入的数据的呢?又是如何使用这些数据的呢?
我们再来看看 callInvoke
方法(也就是
Injector
的 Invoke
实现):
1 | func (inj *injector) callInvoke(f interface{}, t reflect.Type, numIn int) ([]reflect.Value, error) { |
参数和返回值说明:
- 第一个参数是我们 Invoke 的函数,这个函数的参数,都会通过 Injector 根据函数参数类型获取
- 第二个参数 f 的反射类型,也就是 reflect.TypeOf(f)
- 第三个参数是 f 的参数个数
- 返回值是
reflect.Value
切片,如果我们在调用过程出错,返回error
在这个函数中,会通过反射来获取 f
的参数类型(reflect.Type
),拿到这个类型之后,从
Injector
里面获取我们之前注入的依赖,这样我们就可以拿到所有参数对应的值。最后,通过
reflect.ValueOf(f)
来调用 f
函数,参数是我们从
Injector
获取到的值的切片。调用之后,返回函数调用结果,一个
reflect.Value
切片。
当然,这只是其中一种使用依赖的方式,另外一种方式也比较常见,就是为结构体注入依赖,这跟
hyperf
里面通过注释注解又或者 Spring
里面的注入方式有点类似。在 Injector
里面是通过
Apply
来为结构体字段注入依赖的:
1 | // 参数 val 是待注入依赖的结构体 |
简单来说,Injector
里面,通过 TypeMapper
来注入依赖,然后通过 Apply
或者 Invoke
来使用注入的依赖。
例子
还是以一开始的例子为例,通过依赖注入的方式来改造一下:
1 | a := A{} |
这个例子中,我们通过 inj.Map
来注入了依赖,在后续通过
inj.Invoke
来调用 test
函数的时候,将会从依赖注入容器里面获取 test
的参数,然后将这些参数传入 test
来调用。
这个例子也许比较简单,但是如果我们很多地方都需要用到 C
这个参数的话,我们通过 inj.Invoke
的方式来调用函数就可以避免每一次调用都要实例化 C
的繁琐操作了。