依赖注入是常见的解耦方式之一。如果系统没有解耦,单元测试就无从谈起。依赖注入指的是显式指定它所需要的功能来执行其任务。早在 1996 年,Robert Martin 就写了一篇文章,名为 The Dependency Inversion Principle 依赖转置原则

隐式接口实现依赖注入

在 Go 语言中实现依赖注入很容易,不需要额外的库。如果建一个非常的 Web 程序,写一个工具函数,用于日志采集:

1
2
3
func LogOutput(message string) {
fmt.Println(message)
}

这个程序还需要数据存储:

1
2
3
4
5
6
7
8
type SimpleDataSource struct {
userData map[string]string
}

func (sds SimpleDataSource) UserNameForID(userID string) (string, bool) {
name, ok := sds.userData[userID]
return name, ok
}

实现一个工厂函数对 SimpleDataSource 进行实例化:

1
2
3
4
5
6
7
8
9
func NewSimpleDataSource() SimpleDataSource {
return SimpleDataSource{
userData: map[string]string{
"1": "Fred",
"2": "Mary",
"3": "Pat",
},
}
}

我们需要实现一些业务逻辑,这个业务逻辑依赖数据存储和日志,但是我们并不想依赖具体的 LogOutputSimpleDataSource。日后我们很可能换成其它的日志记录器或者数据存储的方法。

因此我们需要的是依赖的接口,而不是具体类型。因此,我们对于描述了我们期望的数据存储和日志记录器的接口:

1
2
3
4
5
6
7
type DataStore interface {
UserNameForID(userID string) (string, bool)
}

type Logger interface {
Log(message string)
}

为了使得 LogOutput 满足 Log 的接口,我们定义一个函数类型并实现接口:

1
2
3
4
5
type LoggerAdapter func(message string)

func (lg LoggerAdapter) Log(message string) {
lg(message)
}

LoggerAdapterSimpleDataStore 恰好满足了业务逻辑所需要的接口,但其实这两种类型都不知道它的存在。

具体业务逻辑中,我们指定需要一个日志和数据存储,但我们并不在乎实现,而只是约定必须有对应的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type SimpleLogic struct {
l Logger
ds DataStore
}

func (sl SimpleLogic) SayHello(userID string) (string, error) {
sl.l.Log("in SayHello for " + userID)
name, ok := sl.ds.UserNameForID(userID)
if !ok {
return "", errors.New("unknown user")
}
return "Hello, " + name, nil
}

func (sl SimpleLogic) SayGoodbye(userID string) (string, error) {
sl.l.Log("in SayGoodbye for " + userID)
name, ok := sl.ds.UserNameForID(userID)
if !ok {
return "", errors.New("unknown user")
}
return "Goodbye, " + name, nil
}

这里的隐式接口和 Java 的显式接口非常不同。尽管 Java 使用接口将实现与接口解耦,但显式接口还是将客户端代码和提供者绑定在一起。这使得在 Java(和其他有显式接口的语言)中替换一个依赖项比在 Go 中更加困难。

同样提供一个工厂函数:

1
2
3
4
5
6
func NewSimpleLogic(l Logger, ds DataStore) SimpleLogic {
return SimpleLogic{
l: l,
ds: ds,
}
}

Web 服务

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
type Logic interface {
SayHello(userID string) (string, error)
}

type Controller struct {
l Logger
logic Logic
}

func (c Controller) HandleGreeting(w http.ResponseWriter, r *http.Request) {
c.l.Log("In SayHello")
userID := r.URL.Query().Get("user_id")
message, err := c.logic.SayHello(userID)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
_, _ = w.Write([]byte(message))
}

func NewController(l Logger, logic Logic) Controller {
return Controller{
l: l,
logic: logic,
}
}

func main() {
l := LoggerAdapter(LogOutput)
ds := NewSimpleDataSource()
logic := NewSimpleLogic(l, ds)
c := NewController(l, logic)
http.HandleFunc("/hello", c.HandleGreeting)
_ = http.ListenAndServe(":8080", nil)
}

主函数是代码中唯一知道所有具体类型实际是什么的部分。如果我们想换成不同的实现,修改主函数即可。通过依赖注入将依赖项向更外部转移,意味着我们将随着时间的推移限制对代码的修改。

Google Wire:依赖注入生成

如果觉得手动写依赖注入太麻烦了,可以考虑使用 Wire。它使用代码生成自动创建在主函数中编写的具体类型声明。

Wire 中有两个核心的概念:

  • 供应器:provider
  • 注入器:injector

定义供应器

最简单的「供应器」就是一个函数,返回一个值:

1
2
3
4
5
6
7
8
type Foo struct {
X int
}

// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
return Foo{X: 42}
}

可以在「供应器」中指定依赖:

1
2
3
4
5
6
7
8
type Bar struct {
X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}

这就意味着,ProvideBar 依赖 Foo 产生值。

注入器

「注入器」连接这些「供应器」,它会按依赖顺序调用「供应器」的函数。

用 wire 重写上述例子

只需要按函数参数的接口类型(不能直接通过具体类型自动转换,所以还是要单独提供 Provider)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func LoggerProvider() Logger {
return LoggerAdapter(LogOutput)
}

func SimpleDataSourceProvider() DataStore {
return NewSimpleDataSource()
}

func SimpleLogicProvider(l Logger, ds DataStore) Logic {
return NewSimpleLogic(l, ds)
}

var SimpleLogicSet = wire.NewSet(LoggerProvider, SimpleDataSourceProvider, SimpleLogicProvider)
var ControllerSet = wire.NewSet(SimpleLogicSet, NewController)

编写注入器:

1
2
3
4
5
6
7
8
9
10
11
//go:build wireinject
// +build wireinject

package main

import "github.com/google/wire"

func InitController() Controller {
wire.Build(ControllerSet)
return Controller{}
}

main 中就看不到具体组装过程了:

1
2
3
4
5
6
7
8
9
10
package main

import "net/http"

func main() {
c := InitController()
http.HandleFunc("/hello", c.HandleGreeting)
_ = http.ListenAndServe(":8080", nil)
}

小结

Go 的隐式接口相比于 Java 的显式接口更容易解耦。如果不想手动编写注入的过程,推荐使用 Google 的 wire 自动组装代码。