谈谈 golang 下的依赖注入

需求不断堆砌,代码逻辑越来越复杂,依赖越来越多,该如何组织代码结构,保持各个模块解耦?
本文来谈一下在 golang 中的依赖注入问题。

依赖注入的简单解释

structs

什么是依赖注入,来看维基百科的定义:

在软件工程中,依赖注入(dependency injection)的意思为,给予调用方它所需要的事物。
  • “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入” 。
  • “注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。
  • 传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。

简而言之,外部的依赖不应该由自身创建,而是应该从外部将依赖的对象注入。 例如:

ServerConfig
  • 耦合性高:Server 承担了 Config 的初始化,如果 Config的初始化过程发生改变,我们需要一并修改Server;
  • 扩展性差:一个Server的实例化过程只能构造自己的Config,它不能使用其它已经实例化好的Config;
  • 不利于单元测试:在完成单元测试的时候,我们无法测试不同类型的Config对于Server的影响。

为此我们可以修改成下面这样:

这样,我们可以在外部对Config进行初始化然后注入到Server中。上述修改完成后,耦合性和扩展性的问题便解决掉了,对于Server来说,接收从外部传入的Config,其不关心Config的初始化的过程;另外,多个Server之间可共用一份Config,并且可做到随意替换;此外,测试的时候我们可以较为轻松对关键部件进行替换。

依赖注入的好处

依赖注入处理的关键问题是解耦,解耦在代码工程学中的好处显而易见:代码扩展性强,可维护性增强以及更容易的进行单元测试

依赖注入到哪里

被依赖的模块,在创建模块时,被注入到(即当作参数传入)模块的里面。

Golang的依赖注入框架

项目越来越大,开发合作的同学越来越多,项目启动的依赖也越来越多,而且依赖之间还有先后顺序,甚至还存在一些隐式的初始化顺序,如下述一段代码中:

在工程中实际的情形可远远要比上述情况复杂,此时如果存在框架来帮我们管理依赖,那可能会省心很多。

依赖注入框架的分类

Golang 的依赖注入框架有两类

使用 dig 功能会强大一些,它们都是使用反射机制来实现运行时依赖注入(runtime dependency injection), 但是缺点就是错误只能在运行时才能发现,这样如果不小心的话可能会导致一些隐藏的 bug 出现。

wire则是采用代码生成的方式来达到编译时依赖注入(compile-time dependency injection), 使用 wire 的缺点就是功能限制多一些,但是好处就是编译的时候就可以发现问题,并且生成的代码其实和我们自己手写相关代码差不太多,更符合直觉,心智负担更小,十分容易理解和调试。所以更加推荐 wire.

iWire框架

go get github.com/google/wire/cmd/wirewire$GOPATH/bin$GOPATH/bin$PATHwire

Provider & Injector

在 iwire 中,有两个重要的概念:Provider和Injector

Provider: a function that can produce a value. These functions are ordinary Go code.

Injector: a function that calls providers in dependency order. With Wire, you write the injector's signature, then Wire generates the function's body.

Provider:生成组件的普通方法。这些方法接收所需依赖作为参数,创建组件并将其返回。组件可以是对象或函数 —— 事实上它可以是任何类型,但单一类型在整个依赖图中只能有单一provider。 典型的provider如下:

实践中, 一组业务相关的provider时常被放在一起组织成 ProviderSet,以方便维护与切换。

wire
wire.gowire.Build
wire.gopanic
wire

上述代码有两点值得关注:

// **_+build_** wireinjectwireinject//**_+build_** !wireinject
go generatewire//**_go:generate_** wirewire

然后我们就可以使用真正的injector了, 例如:

wirewire.goNewDb

同样道理, 如果在wire.go 中写入了未使用的provider , 也会有明确的错误提示。


附录

S.O.L.I.D - 面向对象五大设计原则

  • SRP The Single Responsibility Principle 单一责任原则
  • OCP The Open Closed Principle 开放封闭原则
  • LSP The Liskov Substitution Principle 里氏替换原则
  • ISP The Interface Segregation Principle 接口分离原则
  • DIP The Dependency Inversion Principle 依赖倒置原则

参考文章