前言:
- 本文主要介绍的是Goalng中关于 DI 的部分,前一部分会先通过典型的面向对象语言Java引入DI这个概念
- 仅供初学者理解使用,文章如有纰漏敬请指出
- 本文涉及到的知识面较为零散,其中包含面向对象编程的 SOLID原则、各语言典型的DI框架等,博主都已插入连接????供读者访问自行查阅
- 另外本文篇幅较长,粗略阅读全文大概需要5分钟,希望能在看完一遍之后对读者理解DI有所帮助,初步理解什么是依赖注入,并在实践时知道什么时候使用它
正文:
什么是DI
在理解它在编程中的含义之前,首先让我们了解一下它的总体含义,这可以帮助我们更好地理解这个概念。
依赖是指依靠某种东西来获得支持。比如我会说我们对手机的依赖程度过高。
在讨论依赖注入之前,我们先理解编程中的依赖是什么意思。
当 class A 使用 class B 的某些功能时,则表示 class A 具有 class B 依赖。
在 Java 中,在使用其他 class 的方法之前,我们首先需要创建那个 class 的对象(即 class A 需要创建一个 class B 实例)。
因此,将创建对象的任务转移给其他 class,并直接使用依赖项的过程,被称为“依赖项注入”。
依赖注入(Dependency Injection, DI)是一种设计模式,也是Spring框架的核心概念之一。其作用是去除Java类之间的依赖关系,实现松耦合,以便于开发测试。为了更好地理解DI,先了解DI要解决的问题。
我们先用Java代码理解一下普遍的情况:·
耦合太紧的问题
如果使用一个类,自然的做法是创建一个类的实例:
class Player{ Weapon weapon; Player(){ // 与 Sword类紧密耦合 this.weapon = new Sword(); } public void attack() { weapon.attack(); } }
这个方法存在耦合太紧的问题,例如,玩家的武器只能是剑Sword
,而不能把Sword
替换成枪Gun
。要把Sword
改为Gun
,所有涉及到的代码都要修改,当然在代码规模小的时候这根本就不是什么问题,但代码规模很大时,就会费时费力了。
依赖注入(DI)过程
依赖注入是一种消除类之间依赖关系的设计模式。例如,A类要依赖B类,A类不再直接创建B类,而是把这种依赖关系配置在外部xml文件(或java config文件)中,然后由Spring容器根据配置信息创建、管理bean类。
示例:
class Player{ Weapon weapon; // weapon 被注入进来 Player(Weapon weapon){ this.weapon = weapon; } public void attack() { weapon.attack(); } public void setWeapon(Weapon weapon){ this.weapon = weapon; } }
如上所示,Weapon
类的实例并不在代码中创建,而是外部通过构造函数传入,传入类型是父类Weapon
,所以传入的对象类型可以是任何Weapon
子类。
传入哪个子类,可以在外部xml文件(或者java config文件)中配置,Spring容器根据配置信息创建所需子类实例,并注入Player
类中,如下所示:
<bean id="player" class="com.qikegu.demo.Player"> <construct-arg ref="weapon"/> </bean> <bean id="weapon" class="com.qikegu.demo.Gun"> </bean>
上面代码中<construct-arg ref="weapon"/>
ref指向id="weapon"
的bean,传入的武器类型是Gun
,如果想改为Sword
,可以作如下修改:
<bean id="weapon" class="com.qikegu.demo.Sword"> </bean>
注意:松耦合,并不是不要耦合。A类依赖B类,A类和B类之间存在紧密耦合,如果把依赖关系变为A类依赖B的父类B0类,在A类与B0类的依赖关系下,A类可使用B0类的任意子类,A类与B0类的子类之间的依赖关系是松耦合的。
可以看到依赖注入的技术基础是多态机制与反射机制。
有三种类型的依赖注入:
- 构造函数注入:依赖关系是通过 class 构造器提供的。
- setter 注入:注入程序用客户端的 setter 方法注入依赖项。
- 接口注入:依赖项提供了一个注入方法,该方法将把依赖项注入到传递给它的任何客户端中。客户端必须实现一个接口,该接口的 setter 方法接收依赖。
依赖注入的作用是:
- 创建对象
- 知道哪些类需要那些对象
- 并提供所有这些对象
如果对象有任何更改,则依赖注入会对其进行调查,并且不应影响到使用这些对象的类。这样,如果将来对象发生变化,则依赖注入负责为类提供正确的对象。
控制反转——依赖注入背后的概念
这是指一个类不应静态配置其依赖项,而应由其他一些类从外部进行配置。
这是 S.O.L.I.D 的第五项原则——类应该依赖于抽象,而不是依赖于具体的东西(简单地说,就是硬编码)。
根据这些原则,一个类应该专注于履行其职责,而不是创建履行这些职责所需的对象。 这就是依赖注入发挥作用的地方:它为类提供了必需的对象。
使用依赖注入的优势
- 帮助进行单元测试。
- 由于依赖关系的初始化是由注入器组件完成的,因此减少了样板代码。
- 扩展应用程序变得更加容易。
- 帮助实现松耦合,这在应用编程中很重要。
使用依赖注入的劣势
- 学习起来有点复杂,如果使用过度会导致管理问题和其他问题。
- 许多编译时错误被推送到运行时。
- 依赖注入框架是通过反射或动态编程实现的。这可能会妨碍 IDE 自动化的使用,例如“查找引用”,“显示调用层次结构”和安全重构。
你可以自己实现依赖项注入,也可以使用第三方库或框架来实现。
实现依赖注入的库和框架
- Spring (Java)
- Google Guice (Java)
- Dagger (Java and Android)
- Castle Windsor (.NET)
- Unity (.NET)
- Wire(Golang)
重点:Golang TDD 中理解DI
很对人使用Golang时对于依赖注入(dependency injection)存在诸多误解。我们希望本篇会向你展示为什么:
-
你不一定需要一个框架
-
它不会过度复杂化你的设计
-
它易于测试
- 它能让你编写优秀和通用的函数
func Greet(name string) { fmt.Printf("Hello, %s", name) }
那么我们该如何测试它呢?调用 fmt.Printf
会打印到标准输出,用测试框架来捕获它会非常困难。
我们所需要做的就是注入(这只是一个等同于「传入」的好听的词)打印的依赖。
fmt.Printf
的源码,你可以发现一种引入(hook in)的方式:
// It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) }
有意思!在 Printf
内部,只是传入 os.Stdout
,并调用了 Fprintf
。
os.Stdout
究竟是什么?Fprintf
期望第一个参数传递过来什么?
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return }
io.Writer
是:
type Writer interface { Write(p []byte) (n int, err error) }
io.Writer
是一个很好的通用接口,用于「将数据放在某个地方」。Writer 来把问候发送到某处。我们现在来使用这个抽象,让我们的代码可以测试,并且重用性更好。
先写个测试
func TestGreet(t *testing.T) { buffer := bytes.Buffer{} Greet(&buffer,"Chris") got := buffer.String() want := "Hello, Chris" if got != want { t.Errorf("got '%s' want '%s'", got, want) } }
bytes
包中的 buffer
类型实现了 Writer
接口。