---
# System prepended metadata

title: 實例的依賴
tags: [技術分享]

---

實例的依賴
===

###### 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的方式，併發的讓這些初始化各自去執行。

但有依賴的話，實例化就需要等待，怎麽寫出一個專案，對於各項服務或實例依賴有良好的維護性，就看各位開發者的思考了。

