實例的依賴
===
###### 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的方式,併發的讓這些初始化各自去執行。
但有依賴的話,實例化就需要等待,怎麽寫出一個專案,對於各項服務或實例依賴有良好的維護性,就看各位開發者的思考了。