依赖注入:解耦,自动填充代码,方便单元测试的优质轮子!
熟悉Java语言的同学一定不陌生,依赖注入(Dependency Injection)是Spring框架中的设计基石,有开发经验的同学一定会熟知它的概念(当然也是面试常考问题)。
然而在Golang中,我发现很多项目的代码缺少了这一部分,这也是由于Golang并不是严格意义上的面向对象的编程语言。本文将从Java与Go常用框架的DI实现方式分析,详细介绍他们之间的区别。
Dependency Injection(DI) Wikipedia官方解释 In software engineering,
dependency injection is a technique in which an object receives other
objects that it depends on, called dependencies. Typically, the
receiving object is called a client and the passed-in (‘injected’)
object is called a service. The code that passes the service to the
client is called the injector. Instead of the client specifying which
service it will use, the injector tells the client what service to
use. The ‘injection’ refers to the passing of a dependency (a service)
into the client that uses it.
先抛结论:
然而,它更重要的一部分原因还在于,它可以有助于上下层的解耦,避免修改一处代码时还需要改动多个位置,适用于业务发展较快的场景,并且更有利于撰写单元测试。
怎么理解上下层解耦与方便单元测试呢?下面给一个简单的例子:
借鉴官方文档,迎候顾客的服务员Greeter需要发送Message招呼客人,事件Event表示Greeter开始招呼客人的事件,很明显,Event依赖Greeter
type Greeter struct {
Message string
}
type Event struct {
Greeter Greeter
}
func NewGreeter(msg string) Greeter {
return Greeter{Message: msg}
}
// 非依赖注入
func NewEventNDI() Event {
gtr := NewGreeter("11")
return Event{Greeter: gtr}
}
// 依赖注入
func NewEvent(gtr Greeter) Event {
return Event{Greeter: gtr}
}
可以看到,如果采用非依赖注入方式(line13-17),在NewEventNDI()时,需要在构造方法内部输入Greeter参数并且初始化,假设Greeter结构改变或方法改变,却需要在NewEventNDI()中大量修改代码,代码耦合程度较大,因此引入了依赖注入的方式进行Event初始化,即将Greeter通过参数的方式“注入到”Event中,两方面的耦合度降低,在测试的时候,可以mock一个Greeter,从而继续进行测试。
Java中有以下几种方式:
作为最常用的Java项目框架,其核心的思想便是通过依赖注入解决业务逻辑耦合的问题
首先需要添加Spring相关的配置依赖pom.xml
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-context-supportartifactId>
<version>4.3.2.RELEASEversion>
dependency>
结合前文的例子,在java中构造相关的Greeter与Event类
public class Greeter{
private String Message;
public Greeter(String msg){
this.Message = msg;
}
}
public class Event {
private Greeter Greeter;
public Event(Greeter gtr) {
this.Greeter = gtr;
}
public void Start(){
String msg = Greeter.Message;
System.out.println(msg);
}
}
修改Spring的配置文件application.xml文件,配置了Event与Greeter两个类之间的依赖关系
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="event" class="com.cmower.java_demo.ioc.Event">
<constructor-arg ref="greeter" />
bean>
<bean id="greeter" class="com.cmower.java_demo.ioc.Greeter">
<property name="Message" value="11"/>
bean>
beans>
上述配置工作完成后,测试代码如下,即使用了依赖注入的方式,将控制权赋予了框架(xml配置文件),可以较好地解决代码耦合度较紧的问题。
public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Event event = (Event) context.getBean("event");
event.Start(); // output: 11
}
}
在Spring2.5以后,框架推出了更加方便的@Autowired注解的自动装配。
只需要将注解添加到成员变量(或构造方法)之前,即可根据名称或指定名根据自动进行装配。
public class Event {
@Autowired
private Greeter Greeter;
public void Start(){
String msg = Greeter.Message;
System.out.println(msg);
}
}
首先基于 xml(Configration) 的配置向 ApplicationContext 注册合适的类,并从 ApplicationContext 请求创建 bean 对象。 然后 ApplicationContext 构建一个依赖关系树并遍历它以创建所需的 bean对象。ApplicationContext作为Spring容器,可以被配置适应各种需要作为实现方式。
启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied就会在IoC容器自动查找需要的bean,并装配给该对象的属性
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
因为Google出品(和Golang语言一个地方诞生),所以也被认为是当前官方的依赖注入实现方式。
官网:https://github.com/google/wire
官网说明: Wire is a code generation tool that automates connecting
components using dependency injection. Dependencies between components
are represented in Wire as function parameters, encouraging explicit
initialization instead of global variables. Because Wire operates
without runtime state or reflection, code written to be used with Wire
is useful even for hand-written initialization.
go get github.com/google/wire/cmd/wire
type Greeter struct {
Message string
}
type Event struct {
Greeter Greeter
}
func NewGreeter(msg string) Greeter {
return Greeter{Message: msg}
}
func NewEvent(gtr Greeter) Event {
return Event{Greeter: gtr}
}
func (e Event) Start() {
msg := e.Greeter.Message
fmt.Println(msg)
}
func main() {
e := InitializeEvent("11")
e.Start()
}
//+build wireinject
func InitializeEvent(msg string) Event {
wire.Build(NewEvent, NewGreeter)
return Event{}
}
func InitializeEvent(msg string) Event {
greeter := NewGreeter(msg)
event := NewEvent(greeter)
return event
}
通过如上方法,即可通过wire自动推导出他们之间的依赖关系,减少手动代码撰写量。
wire有两个最基本的概念:provider和injector
// github.com/google/wire/internal/wire/analyze.go
func solve(fset *token.FileSet, out types.Type, given *types.Typle, set *ProviderSet)([]call, []error){
...
stk := []frame{{t: out}}
dfs:
for len(stk) > 0{
curr := stk[len(stk)-1]
stk = stk[:len(stk)-1]
...
pv := set.For(curr.t) // 判定其类型
...
switch pv := set.For(curr.t);{
case pv.IsArg(): // 属于参数,直接continue
case pv.IsProvider(): // 属于Provider元素
visitedArgs := true
for i := len(p.Args) - 1; i >= 0; i-- {
a := p.Args[i]
if index.At(a.Type) == nil {
if visitedArgs {
stk = append(stk, curr)
visitedArgs = false
}
stk = append(stk, frame{t: a.Type, from: curr.t, up: &curr})
}
}
if !visitedArgs {
continue
}
...
continue dfs
...
calls = append(calls, call{...})
case pv.IsValue(): // 初始值
...
calls = append(calls, call{...})
case pv.IsField(): // 单独结构体struct
...
stk = append(stk, frame{t: a.Type, from: curr.t, up: &curr})
...
continue dfs
...
calls = append(calls, call{...})
default:
panic("unknown return value from ProviderSet.For")
}
}
...
return calls, nil
}
// github.com/google/wire/internal/wire/parse.go
type Provider struct {
...
Args []ProviderInput // 入参
Out []types.Type // 出参
...
}
目前的项目中,往往依赖关系非常复杂,这就需要用到一些进阶的功能。
可参考:https://github.com/google/wire/blob/main/docs/guide.md
官网:https://github.com/facebookarchive/inject
go get github.com/facebookgo/inject
type Greeter struct {
Message string `inject:"message"`
}
type Event struct {
Greeter Greeter `inject:"inline"`
}
func (e Event) Start() {
msg := e.Greeter.Message
fmt.Println(msg)
}
func main(){
// step1: 初始化Graph
var g inject.Graph
var event Event
msg := "22"
// step2: 提供需要建立关系的元素
if err := g.Provide(
&inject.Object{Value: &event},
&inject.Object{
Value: msg,
Name: "message",
},
); err != nil {
fmt.Printf("g.Provide err = %v \n", err)
return
}
// step3: 注入与解析
if err := g.Populate(); err != nil {
fmt.Printf("g.Populate err = %v \n", err)
return
}
event.Start() // output: 22
}
注意:如果struct中的元素不为指针,需要加入inject:"inline"标识;如果需要定义初始化的参数,需要在inject中指定名字(当然不能是inline和private这两个关键字)
与wire的原理相似,首先新建inject.Graph,即为关系推导中图谱,然后Provide接收相关对象,首先必要地提供顶层对象(依赖其他对象的),本项目中即为打招呼的事件event,然后提供底层对象(被依赖对象)必要参数,本案例中即为打招呼的信息Message(通过Name来指定确定对应关系。最后通过Populate()进行注入,类比wire中的injector。
不同之处在于,Facebook 的inject方法采用了反射的方法在运行时进行解析,所以执行效率上会有一定下降。
Java-Spring | Go-Wire | Go-inject | |
---|---|---|---|
便捷程度 | |||
核心原理 | Bean反射与解析 | 配置文件,文件生成 | Tag标识,反射解析 |
额外性能开销 | 反射 | 无 | 反射 |
推广程度 | 高 | 高(7.9k star) | 中(1.4k star) |
除了上述介绍的三种方法,在golang中还有uber的dig工具,仿照java Spring的go-spring工具,其核心思想大致相同,即:标识/注册,提供元素,注入与解析。感兴趣的同学可以深入了解一下。
Go不是OOP的语言,但是又允许有OOP的编程风格
Go项目推荐使用Wire包生成依赖
原因: 额外性能开销很小,普及率较高,文档丰富,功能强大。
虽然但是,其他框架的设计思想还是比较值得进一步借鉴学习的~~