11-依赖注入魔法

思考并回答以下问题:

实验介绍

在本次实验中,我们将讲解大型应用中的技术,依赖注入。这是大型Web应用中必要的技术之一,它可以帮助开发人员减少代码的复杂性和耦合性,提高代码的可维护性和可重用性。

知识点

  • 依赖注入原理
  • 常用依赖注入库
  • 依赖注入实战

依赖注入原理

简介

依赖注入是一种软件设计模式,用于将组件之间的依赖关系从代码中解耦。在大型Web应用程序中,组件通常包括服务、控制器和其他对象,这些对象需要相互协作才能完成其任务。传统的方式是在代码中硬编码这些依赖关系,这将导致代码难以维护、扩展和测试。

通过使用依赖注入,我们可以将组件的依赖项注入到其构造函数或其他方法中,而不是在组件内部硬编码依赖关系。这使得组件之间的依赖关系变得更加灵活,使得更容易进行单元测试和模块重用。

例子

例如,如果我们需要在Gin框架中实现一个任务管理系统中的文件同步模块,那么我们将需要几个模块,其中可能包括数据库操作模块、文件操作模块、数据校验模块。这些模块很可能都是由不同团队编写的,因此,构造方式以及使用方式还有依赖都有可能不同,要将它们组合起来会很复杂。为了解决这个问题,依赖注入就变得很重要。

以下是依赖注入的例子。

如上图,FileSyncService依赖了FileService和DataValidationService,而它们又同时依赖了DatabaseService,这样的话,要手动构造一个FileSyncService就需要考虑对所有依赖关系的处理,这是很繁琐的。通过依赖注入,就可以只注明FileSyncService的直接依赖,例如FileService和DataValidationService,这样将DatabaseService这样的隐式依赖自动化,解决了模块间依赖的复杂性问题。

实现方式

依赖注入的实现方式有多种,其中最常见的是构造函数注入。在构造函数注入中,依赖项通过构造函数的参数传递给组件。例如,如果一个控制器需要一个服务对象来处理数据,那么可以在控制器的构造函数中将服务对象作为参数传递进来。这样,控制器就可以使用该服务对象,而不需要直接引用该服务对象的实例。

除了构造函数注入,还有其他的注入方式,例如属性注入和方法注入。在属性注入中,依赖项通过类的属性来注入。在方法注入中,依赖项通过方法的参数来注入。

无论是哪种注入方式,依赖注入的实现都需要一个依赖注入容器。依赖注入容器是一个对象,它可以管理依赖项的创建和注入。容器中通常包含了应用程序中的所有依赖项,以及这些依赖项之间的关系。容器可以在应用程序启动时创建,以便在需要时可以轻松地获取依赖项。

常用依赖注入库

Dig

Dig是Uber开源的Go语言轻量级依赖注入库。它提供了一种简单的方法来声明和注入依赖项。它使用Go的结构体标记来识别需要注入的字段和方法,并在运行时自动解析依赖项。Dig的设计目标是提供一种简单而灵活的方式来管理应用程序的依赖关系,同时避免过多的模板代码和复杂的配置。Dig使用Go语言的标记来标记需要注入的依赖项和依赖项的类型。这样做的好处是可以在编译时进行类型检查,避免运行时错误。此外,Dig会自动解析依赖项,并将它们注入到目标结构体中,可以避免手动管理依赖项,降低代码的复杂度。

Go Wire

Go Wire是一个用于生成依赖注入代码的代码生成器,它可以为Go应用程序自动生成初始化代码,以提供更好的性能和类型安全。Go Wire旨在生成简单且高效的代码,尽量降低对应用程序的运行时性能的负面影响。Go Wire是基于DSL,也就是说它使用了一种简单而直观的领域特定语言(Domain Specific Language)来定义依赖项。

依赖注入实战

下面,我们将用dig这个依赖注入库来实战体验Go语言的依赖注入。

打开IDE,创建go.mod文件,输入以下内容。

1
2
3
module course

go 1.19

在终端中运行命令go get go.uber.org/dig安装dig库。go.mod会变为如下内容。
1
2
3
4
5
module course

go 1.19

require go.uber.org/dig v1.16.1

安装好后,创建main.go,输入以下内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
"go.uber.org/dig"
)

type Foo struct {
Bar *Bar `inject:""`
}

type Bar struct {
Baz *Baz `inject:""`
}

type Baz struct {
Name string
}

func main() {
c := dig.New()

// 注册依赖项
c.Provide(func() *Baz {
return &Baz{Name: "World"}
})
c.Provide(func(b *Baz) *Bar {
return &Bar{Baz: b}
})
c.Provide(func(b *Bar) *Foo {
return &Foo{Bar: b}
})

// 解析依赖项
var foo *Foo
err := c.Invoke(func(f *Foo) {
foo = f
})
if err != nil {
panic(err)
}

// 输出结果
fmt.Println(foo.Bar.Baz.Name) // 输出: World
}

在终端中运行go run main.go,会得到World的结果。在这个例子中,我们用dig.New()生成了一个容器c,然后在容器中通过c.Provide来注册依赖,然后通过c.Invoke来获取依赖注入后的实例foo,这个*Foo类实例已经自动的被注入了相关依赖项Bar,Baz。而foo.Bar.Baz.Name的结果就是World

实验总结

本次实验介绍了依赖注入的原理、Go语言依赖注入常用库,以及依赖注入的实战练习。在Go语言或Gin框架大型项目中,依赖注入是个非常方便的让复杂模块解耦合的技术和工具,可以非常方便的将盘根错节的模块拆开并有效管理起来。掌握依赖注入对我们开发大型Gin框架网络应用非常有帮助。

0%