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