在第一篇文章中提到過,為了讓程式碼可測,需要用依賴注入的方式來構建我們的物件,而通常我們會在main.go
做依賴注入,這就導致main.go
會越來越臃腫。為了讓單元測試得以順利進行,main.go
犧牲了它本應該纖細苗條的身材。太胖的main.go
可不是什麼好的訊號,本篇將介紹依賴注入框架(wire),致力於幫助main.go
恢復身材。
臃腫的main
在main.go
中做依賴注入,意味著在初始化程式碼中我們要管理:
- 依賴的初始化順序
- 依賴之間的關係
對於小型專案而言,依賴的數量比較少,初始化程式碼不會很多,不需要引入依賴注入框架。但對於依賴較多的中大型專案,初始化程式碼又臭又長,可讀性和維護性變的很差,隨意感受一下:
func main() {
config := NewConfig()
// db依賴配置
db, err := ConnectDatabase(config)
if err != nil {
panic(err)
}
// PersonRepository 依賴db
personRepository := NewPersonRepository(db)
// PersonService 依賴配置 和 PersonRepository
personService := NewPersonService(config, personRepository)
// NewServer 依賴配置和PersonService
server := NewServer(config, personService)
server.Run()
}
複製程式碼
實踐表明,修改有大量依賴關係的初始化程式碼是一項乏味且耗時的工作。這個時候,我們就需要依賴注入框架來幫忙,簡化初始化程式碼。
上述程式碼來自:blog.drewolson.org/dependency-…
使用依賴注入框架——wire
What is wire?
wire是google開源的依賴注入框架。或者引用官方的話來說:“Wire is a code generation tool that automates connecting components using dependency injection”。
Why wire?
除了wire,Go的依賴注入框架還有Uber的dig和Facebook的inject,它們都是使用反射機制來實現執行時依賴注入(runtime dependency injection),而wire則是採用程式碼生成的方式來達到編譯時依賴注入(compile-time dependency injection)。使用反射帶來的效能損失倒是其次,更重要的是反射使得程式碼難以追蹤和除錯(反射會令Ctrl+左鍵失效...)。而wire生成的程式碼是符合程式設計師常規使用習慣的程式碼,十分容易理解和除錯。
關於wire的優點,在官方博文上有更詳細的的介紹:blog.golang.org/wire
How does it work?
本部分內容參考官方博文:blog.golang.org/wire
wire有兩個基本的概念:provider和injector。
provider
provider就是普通的Go函式,可以把它看作是某物件的建構函式,我們通過provider告訴wire該物件的依賴情況:
// NewUserStore是*UserStore的provider,表明*UserStore依賴於*Config和 *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfig是*Config的provider,沒有依賴
func NewDefaultConfig() *Config {...}
// NewDB是*mysql.DB的provider,依賴於ConnectionInfo
func NewDB(info ConnectionInfo) (*mysql.DB, error) {...}
// UserStoreSet 可選項,可以使用wire.NewSet將通常會一起使用的依賴組合起來。
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)
複製程式碼
injector
injector是wire生成的函式,我們通過呼叫injector來獲取我們所需的物件或值,injector會按照依賴關係,按順序呼叫provider函式:
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
// initUserStore是由wire生成的injector
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// *Config的provider函式
defaultConfig := NewDefaultConfig()
// *mysql.DB的provider函式
db, err := NewDB(info)
if err != nil {
return nil, err
}
// *UserStore的provider函式
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
複製程式碼
injector幫我們把按順序初始化依賴的步驟給做了,我們在main.go
中只需要呼叫initUserStore
方法就能得到我們想要的物件了。
那麼wire是怎麼知道如何生成injector的呢?我們需要寫一個函式來告訴它:
- 定義injector的函式簽名
- 在函式中使用
wire.Build
方法列舉生成injector所需的provider
例如:
// initUserStore用於宣告injector的函式簽名
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// wire.Build宣告要獲取一個UserStore需要呼叫到哪些provider函式
wire.Build(UserStoreSet, NewDB)
return nil, nil // 這些返回值wire並不關心。
}
複製程式碼
有了上面的函式,wire就可以得知如何生成injector了。wire生成injector的步驟描述如下:
- 確定所生成injector函式的函式簽名:
func initUserStore(info ConnectionInfo) (*UserStore, error)
- 感知返回值第一個引數是
*UserStore
- 檢查
wire.Build
列表,找到*UserStore
的provider:NewUserStore
- 由函式簽名
func NewUserStore(cfg *Config, db *mysql.DB)
得知NewUserStore
依賴於*Config
, 和*mysql.DB
- 檢查
wire.Build
列表,找到*Config
和*mysql.DB
的provider:NewDefaultConfig
和NewDB
- 由函式簽名
func NewDefaultConfig() *Config
得知*Config
沒有其他依賴了。 - 由函式簽名
func NewDB(info *ConnectionInfo) (*mysql.DB, error)
得知*mysql.DB
依賴於ConnectionInfo
。 - 檢查
wire.Build
列表,找不到ConnectionInfo
的provider,但在injector函式簽名中發現匹配的入參型別,直接使用該引數作為NewDB
的入參。 - 感知返回值第二個引數是
error
- ....
- 按依賴關係,按順序呼叫provider函式,拼裝injector函式。
舉個例子
栗子傳送門:wire-examples
注意
截止本文釋出前,官方表明wire的專案狀態是alpha,還不適合到生產環境,API存在變化的可能。
雖然是alpha,但其主要作用是為我們生成依賴注入程式碼,其生成的程式碼十分通俗易懂,在做好版本控制的前提下,即使是API發生變化,也不會對生成環境造成多壞的影響。我認為還是可以放心使用的。
總結一下
本篇是本系列的最後一篇,回顧前幾篇文章,我們以單元測試的原理與基本思想為基礎,介紹了表格驅動測試方法,gomock,testify,wire這幾樣實用工具,經歷了“能寫單元測試”到“寫好單元測試”不斷優化的過程。希望本系列文章能讓你有所收穫。