依賴注入(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/> ![](https://i.imgur.com/UFGMQij.png) ### 問題思考 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: 製作蛋糕的過程,需要先將水、麵粉、蛋、砂糖等材料攪拌後,放進烤箱烘焙。 水、麵粉、蛋、砂糖等材料攪拌(加入材料的順序沒差) <-(依賴) 攪拌 <-(依賴) 烘焙。 上述狀況在說明什麼?有時候後續的步驟,需要依賴前面的步驟完成,前面的步驟是否完成,可能等待一項,或多項前置工作一起完成,才算完成。 若此時有個『容器』管理每個必要階段,後面工作只要等待前面『容器』的階段完成即可。 如下圖示意: ![](https://i.imgur.com/fdF16PB.png) 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進階能力,可以多多研究。