實例的依賴 === ###### tags: `技術分享` ## 0. 緣起 [依賴注入(Dependency Injection) Part Of Golang Wire](https://hackmd.io/@akilakuma/HJ79Apfe8) 一篇的原本問題範例,有收到feedback,寫得不是很好懂,於是延伸一篇來跟大家聊聊我認識的golang,在實作上的實例依賴處理。 <br/> ## 1. 何謂依賴 先定義一下,接下來討論的『依賴』,明確是指哪個部分。 『依賴』是指當你需要建立新的實例的時候,需要某些額外的資料或者別的已經建立好的實例,有滿足需要的條件,才能建立完整的實例。 ### 1-1. 實例與實例化 在golang的建立實例,是指如下面範例,將一個Event的struct初始化之後做使用。 初始化Event那個struct的動作,就是我們要討論的實例化。 event 就是實例化之後,得到的實例。 也就是說event,已經是個實例。 ``` go func main() { // 初始化Event{} // 放到event這個變數 event := Event{} } // Event 事件資料 type Event struct { Name string Period time.Duration IsRepeat bool Action func() error } ``` ### 1-2. 滿足實例所需要的資料 如上圖欄位,Event有Name、Period、IsRepeat、Action,4個欄位儲存指定的資料。 實作上,我們也需要真正對他們塞入資料,空空的Event沒有東西,對我們一點幫助也沒有。 例如這樣: ``` go event := Event{ Name: "run_batch", Period: time.Duration(20) * time.Second, IsRepeat: true, Action: OrderReCheck, } ``` ### 1-3. 實作上的程式架構 這樣寫實在很醜,如果一個function裡面需要很多種不同的實例,那麼很容易因為這樣的寫法填滿整個程式區域,試想有些結構比較龐大的struct,需要填上10個以上的欄位,已經花了幾百行的程式碼在做實例化,但其實你想做的處理邏輯都還沒寫。 所以實例化的過程,往往借助method的方式提供。 例如: ``` go func main() { // 初始化Event // 放到event這個變數 event := NewEvent("run_batch", 20*time.Second, true, OrderReCheck) } // NewEvent 新的Event func NewEvent(n string, per time.Duration, rp bool, act func() error) *Event { return &Event{ Name: n, Period: per, IsRepeat: rp, Action: act, } } ``` ### 1-4. 依賴開始出現 上述範例,為了滿足使用NewEvent(),你一定要準備指定的那4個資料,分別是input,Name、Period、IsRepeat、Action,否則就無法使用這個function,編譯時期就會報錯。 於是你可以發現NewEvent(),依賴input,Name、Period、IsRepeat、Action,這就是所謂的『依賴』。 <br/> ## 2. 實作上的變形 ### 2-1. 如果不是每項input都絕對必要 不是絕對的必要,換句話說,也就是非絕對的依賴。 以上面舉例,假設Name不是絕對需要的,那麼可以調整一下寫法。 ``` go // NewEvent 新的Event func NewEvent(per time.Duration, rp bool, act func() error) *Event { return &Event{ Period: per, IsRepeat: rp, Action: act, } } // SetName 設定事件名稱 func (e *Event) SetName(n string) { e.Name = n } func main() { // 初始化Event // 放到event這個變數 event := NewEvent(20*time.Second, true, OrderReCheck) event.SetName("super_batch") } ``` 如此一來,依賴只剩3項,使用SetName與否,也可以讓開發者自行決定,或者在別的時機才使用,並不一定要在實例化的時候,馬上給定Name。 ### 2-2. 當實例依賴實例 這樣的需求,會讓事情開始進入複雜,也是為什麼我們會遇到依賴的麻煩起源。 ``` go // EventController 事件控制體 type EventController struct { id int event Event } func main() { eventReport := Event{ Name: "generateReport", Period: 5 * time.Second, IsRepeat: true, Action: BuildRepor, } eventReportController := &EventController{ id: 1, event: eventReport, } } ``` 如上面程式碼,eventReportController可否實例化,強烈依賴著Event實例,換句話說,Event若沒有先實例化,然後提供給eventReportController,eventReportController就沒辦法實例化。 而類似這樣的結構,在golang裡面常見的出現,各個實例彼此有強烈的耦合和依賴,這是非常糟糕的災難。 ### 2-3. 用指標讓原本依賴的對象有延後的生成機會 如果實例依賴的是某個實例的指標,那麼實例和實例彼此之間的關係,就可以獲得改善和某個程度上的鬆綁。 原因很簡單,指標的zero value是nil,而自行設計struct的zero value,是實例化struct後,裡面的各項field是各種形態的zero value。 我們改寫一下程式碼 ``` go // EventController 事件控制體 type EventController struct { id int event *Event } func main() { eventReportController := &EventController{ id: 1, } // 中間程式碼 1 .... // 檢查真的有依賴的實例給進這個struct,才做事情 if eventReportController.event != nil { // 執行邏輯 } // 中間程式碼 2 .... eventReport := &Event{ Name: "generateReport", Period: 5 * time.Second, IsRepeat: true, Action: BuildRepor, } eventReportController.event = eventReport } ``` ### 2-4. 或者依賴的對象,本身並不需要外面的依賴 那麼把依賴對象的實例化method,在自己的實例化過程中使用就好了。 這寫法也很常見。 ``` go type Message struct { RWLocker *sync.RWMutex MMap map[int]string } type MMController struct { m *Message } func NewMessage() *Message { return &Message{ RWLocker: new(sync.RWMutex), MMap: make(map[int]string, 0), } } func NewMMControl() *MMController { return &MMController{ m: NewMessage(), } } ``` 以這個例子,NewMessage()並沒有外部的依賴,所以某方面來講,MMController並沒有什麼必定依賴的對象,它可以自己搞定實例化。 但是,也並非因此就世界和平,global variable 讓整件事情變得不安全。 <br/> ## 3. Global Variable 的探討 ### 3-1. 聲明 接下來的討論以全域變數為主軸,全域變數在於實例與實例的依賴關係扮演的角色。 用法和寫法是參考golang standard package,我相信也是一堆人這樣學這樣用。 這樣用法的有其缺點,接下來會介紹,但不可否置的,因為有實際的需求,所以才有這樣的用法,使用時機和考量就交給各自開發人員自行判斷。 ### 3-2. 範例 如下範例 conn.CenterConnector 是個全域變數,讓外面的package可以使用。 ``` go // file: // try/main.go package main import ( "fmt" conn "try/connect" ) func main() { // 建立符合Driver interface的實例 qqDriver := &MyQQDriver{} // 將qqDriver注入到全域變數 conn.Init("192.168.2.1", qqDriver) // 因為依賴的內容是從全域變數獲得,在此處看不出明顯和上一行的依賴關係 conn.CenterConnector.Driver.Build() } type MyQQDriver struct{} func (mqq *MyQQDriver) Build() error { fmt.Println("build") return nil } func (mqq *MyQQDriver) Detect() error { return nil } ``` ``` go // file: // try/connect/conn.go package connect var CenterConnector *Connector type Driver interface { Build() error Detect() error } type Connector struct { connInfo string Driver Driver } func Init(conn string, d Driver) { CenterConnector = NewConnector(conn, d) } func NewConnector(conn string, d Driver) *Connector { return &Connector{ connInfo: conn, Driver: d, } } ``` ### 3-3. 用法探討 conn.CenterConnector型態是個指標,換句話說,可能是nil。 什麼時候依賴的對象實例準備好了,再注入到conn.CenterConnector即可。 #### 好處 1. 要注入的是 connInfo 和 Driver,需要的Driver不一定能夠搶在Connector實例化之前,來得及先實例化完畢。理想狀態是Driver能夠實例化之後,再實例化Connector。 2. 實例化Connector應該受限於某些程式碼去執行,而不是整個project要使用之前,能夠無差別就去執行實例化Connector,尤其是需要網路連線的功能或服務,這樣的實例應該非常嚴格控管數量。 3. 要有一個大家都可以知道,並且使用的識別子,所以當仁不讓,有個大寫開頭的全域變數,最適合當這個package對外的接口。 #### 壞處 1. 如果沒有事先呼叫過Init()做注入的動作,直接使用method,例如使用conn.CenterConnector.Driver.Build(),程式就發生panic。防範方法,是先確認CenterConnector是否為nil。 2. 不易直覺了解彼此實例的依賴關係。 3. 注入過程中若發生失敗,不易處理。 ### 3-4. 結論 這樣的solution我自己認為優點還是遠大於缺點,所以並不會為了避開global variable可能導致的問題,而不使用這樣的寫法。 global variable讓依賴問題更加複雜,考慮程式的維護性,開發者在使用上應該更小心斟酌。 即使使用了DI套件,如[wire](https://github.com/google/wire)或[dig](https://github.com/uber-go/dig),也許也還是可以搭配global variable的方式一起使用,兩者解決的問題面向不盡然相同。 <br/> ## 4. 補充 有些服務或實例的初始化,彼此可能不相關,有的服務初始化可能需要額外時間(例如說建立連線),可以考慮用goroutine的方式,併發的讓這些初始化各自去執行。 但有依賴的話,實例化就需要等待,怎麽寫出一個專案,對於各項服務或實例依賴有良好的維護性,就看各位開發者的思考了。