前言:

  • 本文主要介绍的是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 自动化的使用,例如“查找引用”,“显示调用层次结构”和安全重构。

  你可以自己实现依赖项注入,也可以使用第三方库或框架来实现。

实现依赖注入的库和框架

重点:Golang TDD 中理解DI

  很对人使用Golang时对于依赖注入(dependency injection)存在诸多误解。我们希望本篇会向你展示为什么:

  • 你不一定需要一个框架
  •  它不会过度复杂化你的设计
  •  它易于测试
  • 它能让你编写优秀和通用的函数
  就像我们在 hello-world 做的那样,我们想要编写一个问候某人的函数,只不过这次我们希望测试实际的打印(actual printing)。
  这个函数应该长这个样子:

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)
}
如果你写过很多 Go 代码的话,你会发现这个接口出现的频率很高,因为 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 接口。
因此,我们可以在测试中,用它来作为我们的 Writer,接着调用了 Greet后,我们可以用它来检查写入了什么。  
 

尝试运行测试

./di_test.go:10:7: too many arguments in call to Greet
have (*bytes.Buffer, string)
want (string)

编写最小化代码供测试运行,并检查失败的测试输出

根据编译器提示修复问题。

func Greet(writer *bytes.Buffer, name string) {
fmt.Printf("Hello, %s", name)
}

Hello, Chris di_test.go:16: got '' want 'Hello, Chris'

测试失败了。注意到可以打印出 name,不过它传到了标准输出

编写足够的代码使其通过

writer 把问候发送到我们测试中的缓冲区。记住 fmt.Fprintffmt.Printf 一样,只不过 fmt.Fprintf 会接收一个 Writer 参数,用于把字符串传递过去,而 fmt.Printf 默认是标准输出。

func Greet(writer *bytes.Buffer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
}

现在测试就可以通过了。

重构

早些时候,编译器会告诉我们需要传入一个指向 bytes.Buffer 的指针。这在技术上是正确的,但却不是很有用。
为了展示这一点,我们把 Greet 函数接入到一个 Go 应用里面,其中我们会打印到标准输出。

func main() {
Greet(os.Stdout, "Elodie")
}

./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet

我们前面讨论过,fmt.Fprintf 允许传入一个 io.Writer 接口,我们知道 os.Stdoutbytes.Buffer 都实现了它

我们可以修改一下代码,使用更为通用的接口,这样我们现在可以在测试和应用中都使用这个函数了

package main

import (
"fmt"
"os"
"io"
) func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
} func main() {
Greet(os.Stdout, "Elodie")
}

关于 io.Writer 的更多内容

通过使用 io.Writer,我们还可以将数据写入哪些地方?我们的 Greet 函数的通用性怎么样了?

互联网

运行下面代码:

package main

import (
"fmt"
"io"
"net/http"
) func Greet(writer io.Writer, name string) {
fmt.Fprintf(writer, "Hello, %s", name)
} func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
Greet(w, "world")
} func main() {
http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler))
}

运行程序并访问 http://localhost:5000。你会看到你的 greeting 函数被使用了。

当你编写一个 HTTP 处理器(handler)时,你需要给出 http.ResponseWriter 和用于创建请求的 http.Request。在你实现服务器时,你使用 writer 写入了请求。
你可能已经猜到,http.ResponseWriter 也实现了 io.Writer,所以我们可以重用处理器中的 Greet函数。
 

总结

我们第一轮迭代的代码不易测试,因为它把数据写到了我们无法控制的地方。
通过测试的启发,我们重构了代码。因为有了注入依赖,我们可以控制数据向哪儿写入,它允许我们:

  • 测试代码。如果你不能很轻松地测试函数,这通常是因为有依赖硬链接到了函数或全局状态。例如,如果某个服务层使用了全局的数据库连接池,这通常难以测试,并且运行速度会很慢。DI 提倡你注入一个数据库依赖(通过接口),然后就可以在测试中控制你的模拟数据了。
  •  关注点分离,解耦了数据到达的地方如何产生数据。如果你感觉一个方法 / 函数负责太多功能了(生成数据并且写入一个数据库?处理 HTTP 请求并且处理业务级别的逻辑),那么你可能就需要 DI 这个工具了。
  • 在不同环境下重用代码。我们的代码所处的第一个「新」环境就是在内部进行测试。但是随后,如果其他人想要用你的代码尝试点新东西,他们只要注入他们自己的依赖就可以了。
 
 

Go语言:一文看懂什么是DI依赖注入(dependency injection)设计模式的更多相关文章

  1. 一文读懂Asp.net core 依赖注入(Dependency injection)

    一.什么是依赖注入 首先在Asp.net core中是支持依赖注入软件设计模式,或者说依赖注入是asp.net core的核心: 依赖注入(DI)和控制反转(IOC)基本是一个意思,因为说起来谁都离不 ...

  2. 一文看懂web服务器、应用服务器、web容器、反向代理服务器区别与联系

    我们知道,不同肤色的人外貌差别很大,而双胞胎的辨识很难.有意思的是Web服务器/Web容器/Web应用程序服务器/反向代理有点像四胞胎,在网络上经常一起出现.本文将带读者对这四个相似概念如何区分. 1 ...

  3. [转帖]一文看懂web服务器、应用服务器、web容器、反向代理服务器区别与联系

    一文看懂web服务器.应用服务器.web容器.反向代理服务器区别与联系 https://www.cnblogs.com/vipyoumay/p/7455431.html 我们知道,不同肤色的人外貌差别 ...

  4. [转帖] 一文看懂:"边缘计算"究竟是什么?为何潜力无限?

    一文看懂:"边缘计算"究竟是什么?为何潜力无限? 转载cnbeta   云计算 雾计算 边缘计算...   知名创投调研机构CB Insights撰文详述了边缘计算的发展和应用前景 ...

  5. 一文看懂大数据的技术生态圈,Hadoop,hive,spark都有了

    一文看懂大数据的技术生态圈,Hadoop,hive,spark都有了 转载: 大数据本身是个很宽泛的概念,Hadoop生态圈(或者泛生态圈)基本上都是为了处理超过单机尺度的数据处理而诞生的.你可以把它 ...

  6. 【转帖】一文看懂docker容器技术架构及其中的各个模块

    一文看懂docker容器技术架构及其中的各个模块 原创 波波说运维 2019-09-29 00:01:00 https://www.toutiao.com/a6740234030798602763/ ...

  7. 一文看懂https如何保证数据传输的安全性的【转载、收藏】

    一文看懂https如何保证数据传输的安全性的   一文看懂https如何保证数据传输的安全性的 大家都知道,在客户端与服务器数据传输的过程中,http协议的传输是不安全的,也就是一般情况下http是明 ...

  8. 一文看懂Stacking!(含Python代码)

    一文看懂Stacking!(含Python代码) https://mp.weixin.qq.com/s/faQNTGgBZdZyyZscdhjwUQ

  9. Nature 为引,一文看懂个体化肿瘤疫苗前世今生

    进入2017年,当红辣子鸡PD-1疗法,一路横扫多个适应症.而CAR-T治疗的“小车”在获得FDA专委会推荐后也已经走上高速路,成为免疫治疗又一里程碑事件.PD-1.CAR-T之后,下一个免疫治疗产品 ...

  10. 转载来自朱小厮博客的 一文看懂Kafka消息格式的演变

    转载来自朱小厮博客的 一文看懂Kafka消息格式的演变     ✎摘要 对于一个成熟的消息中间件而言,消息格式不仅关系到功能维度的扩展,还牵涉到性能维度的优化.随着Kafka的迅猛发展,其消息格式也在 ...

随机推荐

  1. 高性能的StampedLock锁

    StampedLock 支持的三种锁模式: 1.ReadWriteLock 支持两种模式:一种是读锁,一种是写锁 2.StampedLock 支持三种模式,分别是:写锁.悲观读锁和乐观读 1)写锁.悲 ...

  2. 证书profile 申请

    ios  云打包   证书 和密码  在之前的笔记当中已申请好 证书profile 文件申请 登录苹果开放平台 申请前要添加身份,设备,设备那需要填写udid 方法链接https://www.jian ...

  3. jmeter之【报错记录】

    { "code": "E0001", "success": false, "description": "Co ...

  4. git命令2

    git status git status -s git diff git diff --cached --添加到暂存区成为快照 git add -A git add . --删除被git追踪的文件 ...

  5. 1006.Django模型基础01

    一.Django的ORM简介 1. ORM概念:对象关系映射(Object Relational Mapping): 2. ORM优势:不用直接编写SQL代码,只需像操作对象一样从数据库操作数据. d ...

  6. 在VS中使用Wind数据终端API的经验(一)

    因工作需要,使用vs2019来调用Wind金融终端API数据接口.具体步骤按照wind的帮助文档一步步做下来.这里提一下和帮助文档不同的地方. Windows Console APP下,编译项目后出现 ...

  7. Chrome(谷歌)浏览器永久关闭恢复页面提示框(记录)

    使用脚本调用Chrome浏览器启动指定页面,然后代码里的命令关闭,会被浏览器识别为非正常关闭. 再次执行脚本的时候会停留在空白页面,无法进入指定页面,设置为主页也无法进入. 排查可能是浏览器自动恢复页 ...

  8. JSON反序列化接口的问题

    今天在使用JSON序列化类时出现问题,原来类中有一个接口,在反序列化时不知道接口的实体是什么 public class Device : IComparer {         private str ...

  9. HTML笔记(二) HTML标签元素

    一 常用的头部元素标签 <head>元素包含了所有的头部标签元素. 1.<title> <title>标签定义了HTML文档的标题,在HTML/XHTML文档中是必 ...

  10. .DS_Store泄露利用过程

    .DS_Store文件泄漏利用工具: ds_store_exp.DS_Store是Mac下Finder用来保存如何展示 文件/文件夹 的数据文件,每个文件夹下对应一个. 如果开发/设计人员将.DS_S ...