依賴注入(Dependency Injection)
Part Of Golang Wire
===
###### tags: `技術分享`
## 0. 相關閱讀
[Dependency Injection in Go](https://blog.drewolson.org/dependency-injection-in-go)
[控制反轉 (IoC) 與 依賴注入 (DI)](https://notfalse.net/3/ioc-di)
<br/>
## 1. 對象
### 人員
1. 程式開發工程師。
2. 程式架構設計者。
3. 重構人員。
### 時機
1. 程式設計,遇到多個實例(Instance)或服務(Service),建立時有前後依賴關係。
2. 加新的服務或實例時,與既有服務有依賴關係。
<br/><br/>
## 2. 問題
### 問題範例
* 請見下圖所示,在程式的進入點,最開始的程式需要執行許多初始化動作。
* init() 列了一排初始化method,並且賦予這個順序是有意義,因為怕有人誤調,還在最上方註解說明load config 必須要先執行。
<br/><br/>

### 問題思考
1. 若有需要新增一個服務的初始化,我應該放在這個框框的哪個位置?
2. 這樣的順序,我怎麼知道 Fish1.InitGeneratorFatory()這個初始化動作,是否有依賴上面先出現的 FishLoad.LoadConfig("")或CenterApartment.InitKeeper()初始化?
3. 承2. 說不定上面兩個動作初始化失敗,Fish1.InitGeneratorFatory()照樣能夠初始化。
4. 承2. 如果相依上面兩個動作初始化,Fish1.InitGeneratorFatory()發生失敗了,可能要查看是否為上面兩個動作失敗所導致。
5. 有沒有辦法解開,或更好管理這種相依性呢?
### 說明
其實關於DI的議題,我認為有分為兩個部分,缺了其中一部分,對於DI的理解就像只見到冰山一角。
兩個部分分別是:
1. 解決常見不好觀念:建構依賴在實作,而非依賴抽象。
2. 先後順序有絕對的關係,怎麼管理平行化和有序的依賴性服務,整理做整合工作。
其實第二點只是第一點的延伸,但往往常找到的資訊,通常都只集中在介紹第一點的設計模式。
第一點的好比在介紹製造某個模組零件,要用什麼工法或組合方式。第二點,則是關於怎麼把這些做好的大型模組零件,再組合成一台機器。
而這篇文章以第二點相關討論,如上面的問題範例,來做為大家做DI的介紹。
<br/><br/>
## 3. 解決方案
### 概念
關鍵字:container
有個概念是這樣子,我們用兩個例子簡單形容可能遇到的狀況。
##### 例子1:
首先,我們用蓋房子來形容,要蓋1樓,必須先蓋地基,要蓋2樓,必須先等1樓蓋好,要蓋3樓,必須先蓋好2樓。
地基 <-(依賴) 1樓 <-(依賴) 2樓 <-(依賴) 3樓
##### 例子2:
製作蛋糕的過程,需要先將水、麵粉、蛋、砂糖等材料攪拌後,放進烤箱烘焙。
水、麵粉、蛋、砂糖等材料攪拌(加入材料的順序沒差) <-(依賴) 攪拌 <-(依賴) 烘焙。
上述狀況在說明什麼?有時候後續的步驟,需要依賴前面的步驟完成,前面的步驟是否完成,可能等待一項,或多項前置工作一起完成,才算完成。
若此時有個『容器』管理每個必要階段,後面工作只要等待前面『容器』的階段完成即可。
如下圖示意:

C 容器需要等A和B的服務完成(可視為最小單位的容器),C服務才能啟動。
E 容器需要等C和D的容器完成,才能啟動。
F 容器需要等E容器完成,才能啟動。
F 只要盯著E。
E 盯著C和D,但不用在乎C和D的順序,因為沒有相依性。
C 盯著A和B,但不用在乎A和B的順序,因為沒有相依性。
如果了解php 的 laravel 框架,就知道這是它有名service container應用。
<br/><br/>
## 4. 實作
### 說明
接下來以指定程式語言,golang的立場,說明實作的方式與範例,當然接下來的說明,只是其中一種解法或解決方案,並非唯一選擇。
對於golang不熟悉者,也可以參考接下來說明的解決方案當中,所提及的想法和概念。
### Google的wire
[Wire: Automated Initialization in Go](https://github.com/google/wire)
選用的是google提供的方案,wire是google開發的open source package,轉述[官方文章](https://blog.golang.org/wire)的說明,早在wire之前,分別就有uber開發的[dig](https://github.com/uber-go/dig)和facebook開發的[inject](https://github.com/facebookarchive/inject)提供這樣的DI實作需求。
wire 是採用code generation 的方式。(可以在很多程式語言的框架或套件看到這樣的做法,一點都不奇怪)
如此一來有幾項優點,其中最主要的方便點,在於compile-time 就能知道結果,而非需要到run-time時期,run-time才執行,會有比較高的效能負擔與不易除錯的缺點。
php 的Laravel 框架是走[Service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern)的設計模式。
在這邊幫不清楚的朋友簡單說明,Service locator pattern 也是上面用容器概念說明的設計模式,雖然都是用容器的概念實作DI,但因為程式語言的特性和更具體的實作差異,Service locator pattern在更細部的討論,被特別提出來。
wire不是走Service locator pattern的方式,在Golang因為有鴨子模型設計概念,藉由go type就能夠找到對應的依賴,而非透過名稱或者自定義的Key。
<br/>
### wire的使用方式
wire 的使用內容,簡單分為兩種東西,providers和injectors。
#### 事前安裝
---
```
go get github.com/google/wire/cmd/wire
go get github.com/google/wire
```
#### providers
---
:::info
轉述文件說明,以下面為例:
1. NewUserStore 這個函式,是UserStore這個回傳物件,作為提供者的角色,也就是provider。
2. 依賴了 *Config和 *mysql.DB,兩種傳入參數。
``` go
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
```
providers 時常也會有相依關性,像小容器們放到一個大容器的概念,所以在wire可以將它們做成ProviderSet,如下:
``` go
var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)
```
:::
### injectors
---
:::info
有了準備好的容器,剩下就是注入的工作。
如文件的範例,ConnectionInfo是外部一定要丟入的參數,所以加到function的Input。
wire build 就好比容器,指定有依賴的內容放入。
``` go
func initUserStore(info ConnectionInfo) (*UserStore, error) {
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}
```
:::
### 執行
---
:::info
先在檔案先加build tag
```
//+build wireinject
```
接下來輸入指令
第一次:
``` go
wire
```
之後要再調整可以打以下指令
``` go
go generate
```
wire套件會自動生成相對應的程式碼
```
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
func initUserStore(info ConnectionInfo) (*UserStore, error) {
defaultConfig := NewDefaultConfig()
db, err := NewDB(info)
if err != nil {
return nil, err
}
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
```
:::
<br/><br/>
## 補充
1. 上述code generation的做法,白話文一點,就是照著文件理解,把有依賴的部分放在同一個provider set (容器的概念)。使用方面就是照著injecter的寫法,注入什麼,就會得到什麼。然後用wire重新產生code,最終只使用golang另外產生的code,就不用原本的code,來達到避免人工為了依賴和注入苦惱的效果。
2. golang有提供簡單易懂的tutorial,筆者就不獻醜了。
[範例tutorial](https://github.com/google/wire/tree/master/_tutorial)
3. 一開始問題範例示範,使用的依賴處理綁在全域變數,如[官方文章](https://blog.golang.org/wire)的說明,這會有另外的全域變數麻煩問題,而且另外有所考量之下,不特別拿原本的問題使用wire做改造。
4. 需要搭配golang的build tag,才能使用code generation的方式,提供這樣的解決方案。
5. Laravel使用 Service locator pattern 跟語言特性有關,細節有興趣可以自行研究。
6. golang的build tag 有更多的延伸應用,有興趣學習golang進階能力,可以多多研究。