DI 又稱 Dependency Injection,中文叫依賴注入,似乎很厲害很艱澀難懂,這邊引用一個對我很有幫助,關於DI的的一篇文章「淺入淺出 Dependency Injection」。

What is DI?

淺入淺出 Dependency Injection這篇文章對「依賴注入」用了很精闢的解釋,你可以幫你所有的女朋友都取叫一樣的別名「寶貝」,之後每次換女朋友都統一叫寶貝就好了,不需要叫他們的名字「小美」、「小筠」…之類的。這種透過介面(寶貝)去取代實作(小美、小筠)的行為就可以理解成是反轉注入。

順帶一提,他正好是軟體工程世界中大名鼎鼎的S.O.L.I.D法則的「D」:Dependency Inversion Principle。SOLID WikiPedia

Depend upon abstractions, [not] concretes

一個方法應該遵從「依賴於抽象而不是一個實例」

DI 用電器舉例實作

由於筆者比較擅長golang,本小節會著重於golang的DI實作。 首先一樣用淺入淺出 Dependency Injection舉的例子來改寫~ AI世代,首先請ChatGPT幫我把文章中C#改寫成Golang版本:

package main

import (
	"fmt"
)

// 定義電源插頭接口 Service
type ElectricalPlug interface {
	Connect()
}

// 定義插座結構體 Service
type Socket struct {
	plug ElectricalPlug
}

// 插座結構體的構造函數,進行依賴注入 Service
func NewSocket(plug ElectricalPlug) *Socket {
	if plug == nil {
		panic("plug is nil")
	}

	return &Socket{plug: plug}
}

// 插座結構體的方法,用於發送電源 Service
func (s *Socket) SendPower() {
	s.plug.Connect()
}

// 定義吹風機插頭結構體,實現 ElectricalPlug 接口 Repository
type HairDryerPlug struct{}

// 吹風機插頭結構體的 Connect 方法
func (h *HairDryerPlug) Connect() {
	fmt.Println("HairDryerPlug connected!")
}

func main() {
	hairDryerPlug := &HairDryerPlug{}
	socket := NewSocket(hairDryerPlug)
	socket.SendPower()
}

在這邊一一介紹每個function~ 結構體ElectricalPlug就是我們所定義的「寶貝」介面,在這邊是使用電器的例子,我們可以把「寶貝」理解成「插座」,「插座」目前就是我們抽象化實作所製造出的介面。這個插座會符合他能達到物件導向中「多型」的效果,這些電器都會都會透過呼叫「輸電」的功能來啟動電器。目前雖然只有吹風機的插頭,但之後要加上電風扇、電視機都可以一樣用相同的插座來呼叫「輸電」啟動。

  • 結構體Socket就是將輸電的功能加到插座中,可以看到他將先前所定義的介面ElectricalPlug放到插座中,並取座較plug的別名,之後只要有plug(插頭),都可以透過這個插座進行「輸電」。

  • 函式NewSocket定義了新增電器的方法,之後只要有新電器我們就可以呼叫這個方法,並生成我們所定義的plug(插頭)。

  • 函式SendPower,他再函式中做了兩件事 1.幫我們簡化了呼叫「輸電」的流程,我們可以透過它簡化要呼叫的物件的繁瑣步驟 2. 呼叫「輸電」Connect

  • 結構體HairDryerPlug定義了吹風機的插頭,他能告訴插座他是符合規格的插頭

  • 函式「(h *HairDryerPlug) Connect()」定義了吹風機輸電的實作,當他接收到「輸電」方法所提供的電力後,吹風機會發出通知吹風機已連接,到此就完成了一個「輸電」的週期,插頭部分已經完成實作,並通知吹風機可以啟動了。

  • 最後函式main()就是我們的主程式,真的要動手使用剛剛所定義的所有東西, 透過「hairDryerPlug := &HairDryerPlug{}」我們直接生產製造出了一個吹風機的插頭模組, 「socket := NewSocket(electricFan)」讓剛剛所定義的插頭模組符合我們所定義的插座規格 「socket.SendPower()」進行「輸電」 在此,我們只實作了輸電模組的流程,實際上吹風機的調整風量、調整溫度的方法都是交由吹風機的溫度模組、風量模組完成的!

以上的設計模式讓我們可以無痛新增新的電器,並輕讓所有新電器的插頭都可以輕鬆符合插座規格。

來沿用剛剛定義好的方法來試著新增一個電風扇看看~

package main

import (
	"fmt"
)

// 定義電源插頭接口 Service
type ElectricalPlug interface {
	Connect()
}

// 定義插座結構體 Service
type Socket struct {
	plug ElectricalPlug
}

// 插座結構體的構造函數,進行依賴注入 Service
func NewSocket(plug ElectricalPlug) *Socket {
	if plug == nil {
		panic("plug is nil")
	}

	return &Socket{plug: plug}
}

// 插座結構體的方法,用於發送電源 Service
func (s *Socket) SendPower() {
	s.plug.Connect()
}

// 定義吹風機插頭結構體,實現 ElectricalPlug 接口 Repository
type HairDryerPlug struct{}

// 吹風機插頭結構體的 Connect 方法
func (h *HairDryerPlug) Connect() {
	fmt.Println("HairDryerPlug connected!")
}

type FanPlug struct{}

func (f *FanPlug) Connect() {
    fmt.Println("FanPlug connected!")
}

func main() {
    // 使用吹風機插頭
	hairDryerPlug := &HairDryerPlug{}
	socket := NewSocket(hairDryerPlug)
    socket.SendPower()

    // 使用電風扇插頭
    fanPlug := &FanPlug{}
    socket := NewSocket(fanPlug)
	socket.SendPower()
}

可以看出來我們只要簡單的新增結構體ElectricFanPlug與「(e *ElectricFanPlug) Connect()」電風扇輸電的實作就能輕鬆地符合我們所定義的插座規格並新增電器。

DI實務應用

在我現在這份的工作中,我也有嘗試去使用DI,我負責的項目是將刷卡機去串接第三方支付,讓刷卡機可以去使用街口支付、Line Pay等等…但在開發結帳功能的途中我發現,大家都會使用到查詢的功能,欸~好像可以把第三方支付(街口、Line Pay)當成吹風機、電風扇並將它們符合插座規格使用XD,之後只要新增了一個第三方支付方法,我就可以讓他可以輕鬆符合我所定義的結帳時的Query規格。

// ===============抽象化界面,可以理解成Query第三方的規格(插座規格)=================
type Statistics interface {
	RequestToQuery(requestData *requests.Statistics) error
}

type StatisticsService struct {
	engine Statistics
}

func NewEngine(engine Statistics) *StatisticsService {
	return &StatisticsService{engine: engine}
}

func (s StatisticsService) refundTimeoutQuery(requestData *requests.Statistics) error {
	errMsg := s.engine.RequestToQuery(requestData)
	return errMsg
}

// ========================Line Pay實做(創造插頭)===============================
type LinepayEngine struct{}

func (engine LinepayEngine) RequestToQuery(requestData *requests.Statistics) error {
	_, errMsg := engine.Query(queryRequestData)
	if errMsg != nil {
		return errMsg
	} 

	return nil
}

// ========================街口實做(創造插頭)===============================

type JkosEngine struct{}

func (engine JkosEngine) RequestToQuery(requestData *requests.Statistics) error {
	_, errMsg := engine.Query(queryRequestData)
	if errMsg != nil {
		return errMsg
	} 

	return nil
}

// ================最後運用多型(呼叫相同方法)與DI(符合第三方Query規格[插座規格])==================
func main() {
    errMsg := NewEngine(linepay.LinepayEngine{}).refundTimeoutQuery(requestData)
    if errMsg != nil {
        return errMsg
    }

    errMsg := NewEngine(jkos.JkosEngine{}).refundTimeoutQuery(requestData)
    if errMsg != nil {
        return errMsg
    }
}

重點回顧

  1. DI如何使用抽象化介面,並衍生出介面去規範後續衍伸出的時做
  2. DI用電器舉例他到底怎麼運作
  3. DI商業運用

總結

本文較注重於DI的應用,DI原理的部分看淺入淺出 Dependency Injection大神的文章會比較好理解XD,我只是用Golang的方法與更白話的方式來解釋Golang實際運用場景的一介平民,套一句使人瘋狂的 SOLID 原則:依賴反向原則 (Dependency Inversion Principle)對S.O.L.I.D法則的「D」的解釋:

低層模組的控制權從原來的高層模組中抽離,將兩者的耦合只放在抽象層上。

所有實現的邏輯都會在抽象層介面發生,讓應用層不必接觸到非抽象層,耦合性大幅降低。

就如同本文所舉例的插座,他可以簡單的替換吹風機、電風扇等等,並讓後續所有新增的電器都能符合插座的規格。人生也是如此,你不可能永遠都用一套原則過活,隨著年紀增長你領悟的東西會越來越多,當你套用新的人生原則到原有價值觀時,你也會自行將價值觀介面梳理成可以擴充新人生原則的方式~哈哈哈,我在講啥?我只是想講點有料的東西,結果就如同下面的披薩一樣…沒料!