Skip to content

在 Go 语言中实现依赖注入

发布于  at 04:24 PM

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

隐式接口实现依赖注入

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

func LogOutput(message string) {
	fmt.Println(message)
}

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

type SimpleDataSource struct {
	userData map[string]string
}

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

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

func NewSimpleDataSource() SimpleDataSource {
	return SimpleDataSource{
		userData: map[string]string{
			"1": "Fred",
			"2": "Mary",
			"3": "Pat",
		},
	}
}

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

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

type DataStore interface {
	UserNameForID(userID string) (string, bool)
}

type Logger interface {
	Log(message string)
}

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

type LoggerAdapter func(message string)

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

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

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

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 中更加困难。

同样提供一个工厂函数:

func NewSimpleLogic(l Logger, ds DataStore) SimpleLogic {
	return SimpleLogic{
		l:  l,
		ds: ds,
	}
}

Web 服务

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 中有两个核心的概念:

定义供应器

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

type Foo struct {
    X int
}

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

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

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)。

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)

编写注入器:

//go:build wireinject
// +build wireinject

package main

import "github.com/google/wire"

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

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

package main

import "net/http"

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

小结

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

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

上一篇
槽边往事:准备迎接后疫情时代
下一篇
明智行动的艺术:52 条行动指南