依赖注入是常见的解耦方式之一。如果系统没有解耦,单元测试就无从谈起。依赖注入指的是显式指定它所需要的功能来执行其任务。早在 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",
},
}
}
我们需要实现一些业务逻辑,这个业务逻辑依赖数据存储和日志,但是我们并不想依赖具体的 LogOutput
和 SimpleDataSource
。日后我们很可能换成其它的日志记录器或者数据存储的方法。
因此我们需要的是依赖的接口,而不是具体类型。因此,我们对于描述了我们期望的数据存储和日志记录器的接口:
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)
}
LoggerAdapter
和 SimpleDataStore
恰好满足了业务逻辑所需要的接口,但其实这两种类型都不知道它的存在。
具体业务逻辑中,我们指定需要一个日志和数据存储,但我们并不在乎实现,而只是约定必须有对应的接口。
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 中有两个核心的概念:
- 供应器:provider
- 注入器:injector
定义供应器
最简单的「供应器」就是一个函数,返回一个值:
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 自动组装代码。