理解依赖注入
什么是依赖注入?为什么要依赖注入?
依赖注入 (Dependency Injection,缩写为 DI),可以理解为一种代码的构造模式(就是写法),按照这样的方式来写,能够让你的代码更加容易维护。
对于很多软件设计模式和架构的理念,我们都无法理解他们要绕好大一圈做复杂的体操、用奇怪的方式进行实现的意义。他们通常都只是丢出来一段样例,说这样写就很好很优雅,由于省略掉了这种模式是如何发展出来的推导过程,我们只看到了结果,导致理解起来很困难。那么接下来我们来尝试推导还原一下整个过程,看看代码是如何和为什么演进到依赖注入模式的,以便能够更好理解使用依赖注入的意义。
依赖是什么?
这里的依赖是个名词,不是指软件包的依赖(比如那坨塞在 node_modules 里面的东西),而是指软件中某一个模块(对象/实例)所依赖的其它外部模块(对象/实例)。
注入到哪里?
被依赖的模块,在创建模块时,被注入到(即当作参数传入)模块的里面。
Wire 依赖注入
Wire 是一个灵活的依赖注入工具,通过自动生成代码的方式在编译期完成依赖注入。
在各个组件之间的依赖关系中,通常鼓励显式初始化,而不是全局变量传递。
所以通过 Wire 进行初始化代码,可以很好地解决组件之间的耦合,以及提高代码维护性。
安装工具
# 导入到项目中
go get -u github.com/google/wire
# 安装命令
go install github.com/google/wire/cmd/wire
工作原理
Wire 具有两个基本概念:Provider 和 Injector。
Provider 是一个普通的 Go Func ,这个方法也可以接收其它 Provider 的返回值,从而形成了依赖注入;
// 提供一个配置文件(也可能是配置文件)
func NewConfig() *conf.Data {...}
// 提供数据组件,依赖了数据配置(初始化 Database、Cache 等)
func NewData(c *conf.Data) (*Data, error) {...}
// 提供持久化组件,依赖数据组件(实现 CURD 持久化层)
func NewUserRepo(d *data.Data) (*UserRepo, error) {...}
使用方式
在 Kratos 中,主要分为 server、service、biz、data 服务模块,会通过 Wire 进行模块顺序的初始化;
在每个模块中,只需要一个 ProviderSet 提供者集合,就可以在 wire 中进行依赖注入;
并且我们在每个组件提供入口即可,不需要其它依赖,例如:
-data
--data.go // var ProviderSet = wire.NewSet(NewData, NewGreeterRepo)
--greeter.go // func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {...}
然后通过 wire.go 中定义所有 ProviderSet 可以完成依赖注入配置。
初始化组件
通过 wire 初始化组件,需要定义对应的 wire.go,以及 kratos application 用于启动管理。
// 应用程序入口
cmd
-main.go
-wire.go
-wire_gen.go
// main.go 创建 kratos 应用生命周期管理
func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server, greeter *service.GreeterService) *kratos.App {
pb.RegisterGreeterServer(gs, greeter)
pb.RegisterGreeterHTTPServer(hs, greeter)
return kratos.New(
kratos.Name(Name),
kratos.Version(Version),
kratos.Logger(logger),
kratos.Server(
hs,
gs,
),
)
}
// wire.go 初始化模块
func initApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, error) {
// 构建所有模块中的 ProviderSet,用于生成 wire_gen.go 自动依赖注入文件
panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}
在项目的 main 目录中,运行 wire 进行生成编译期依赖注入代码:
wire
总结
重新总结一下用 wire 做依赖注入的过程。
1. 定义 Injector
创建wire.go文件,定义下你最终想用的实例初始化函数例如initApp(即 Injector),定好它返回的东西*App,在方法里用panic(wire.Build(NewRedis, SomeProviderSet, NewApp))罗列出它依赖哪些实例的初始化方法(即 Provider)/或者哪些组初始化方法(ProviderSet)
2. 定义 ProviderSet(如果有的话)
ProviderSet 就是一组初始化函数,是为了少写一些代码,能够更清晰的组织各个模块的依赖才出现的。也可以不用,但 Injector 里面的东西就需要写一堆。 像这样 var SomeProviderSet = wire.NewSet(NewES,NewDB)定义 ProviderSet 里面包含哪些 Provider
3. 实现各个 Provider
Provider 就是初始化方法,你需要自己实现,比如 NewApp,NewRedis,NewMySQL,GetConfig 等,注意他们们各自的输入输出
4. 生成代码
执行 wire 命令生成代码,工具会扫描你的代码,依照你的 Injector 定义来组织各个 Provider 的执行顺序,并自动按照 Provider 们的类型需求来按照顺序执行和安排参数传递,如果有哪些 Provider 的要求没有满足,会在终端报出来,持续修复执行 wire,直到成功生成wire_gen.go文件。接下来就可以正常使用initApp来写你后续的代码了。
如果需要替换实现,对 Injector 进行相应的修改,实现必须的 Provider,重新生成即可。
评论