依賴(lài)注入與控制反轉(zhuǎn):優(yōu)化Go語(yǔ)言REST API客戶(hù)端
在這篇文章中,我將探討依賴(lài)注入(DI)和控制反轉(zhuǎn)(IoC)是什么,以及它們的重要性。作為示例,我將使用Monibot的REST API客戶(hù)端。讓我們開(kāi)始吧:
一個(gè)簡(jiǎn)單的客戶(hù)端實(shí)現(xiàn)
我們從一個(gè)簡(jiǎn)單的客戶(hù)端實(shí)現(xiàn)開(kāi)始,允許調(diào)用者訪問(wèn)Monibot的REST API,具體來(lái)說(shuō),是為了發(fā)送指標(biāo)值。客戶(hù)端的實(shí)現(xiàn)可能如下所示:
package monibot
type Client struct {
}
func NewClient() *Client {
return &Client{}
}
func (c *Client) PostMetricValue(value int) {
body := fmt.Sprintf("value=%d", value)
http.Post("https://monibot.io/api/metric", []byte(body))
}
這里有一個(gè)客戶(hù)端,提供了PostMetricValue方法,該方法用于將指標(biāo)值上傳到Monibot。我們的庫(kù)的用戶(hù)可能像這樣使用它:
import "monibot"
func main() {
// 初始化API客戶(hù)端
client := monibot.NewClient()
// 發(fā)送指標(biāo)值
client.PostMetricValue(42)
}
依賴(lài)注入
現(xiàn)在假設(shè)我們想對(duì)客戶(hù)端進(jìn)行單元測(cè)試。當(dāng)所有HTTP發(fā)送代碼都是硬編碼的時(shí)候,我們?nèi)绾螠y(cè)試客戶(hù)端呢?對(duì)于每次測(cè)試運(yùn)行,我們都需要一個(gè)“真實(shí)”的HTTP服務(wù)器來(lái)回答我們發(fā)送給它的所有請(qǐng)求。不可取!我們可以做得更好:讓我們將HTTP處理作為“依賴(lài)”;讓我們發(fā)明一個(gè) Transport 接口:
package monibot
// Transport傳輸請(qǐng)求。
type Transport interface {
Post(url string, body []byte)
}
讓我們?cè)侔l(fā)明一個(gè)具體的使用HTTP作為通信協(xié)議的Transport:
package monibot
// HTTPTransport是一個(gè)使用HTTP協(xié)議傳輸請(qǐng)求的Transport。
type HTTPTransport struct {
}
func (t HTTPTransport) Post(url string, data []byte) {
http.Post(url, data)
}
然后讓我們重寫(xiě)客戶(hù)端,使其“依賴(lài)”于一個(gè)Transport 接口:
package monibot
type Client struct {
transport Transport
}
func NewClient(transport Transport) *Client {
return &Client{transport}
}
func (c *Client) PostMetricValue(value int) {
body := fmt.Sprintf("value=%d", value)
c.transport.Post("https://monibot.io/api/metric", []byte(body))
}
現(xiàn)在,客戶(hù)端將請(qǐng)求轉(zhuǎn)發(fā)到它的Transport依賴(lài)。當(dāng)創(chuàng)建客戶(hù)端時(shí),transport(客戶(hù)端的依賴(lài)項(xiàng))被“注入”到客戶(hù)端中。調(diào)用者可以這樣初始化一個(gè)客戶(hù)端:
import "monibot"
func main() {
// 初始化API客戶(hù)端
var transport monibot.HTTPTransport
client := monibot.NewClient(transport)
// 發(fā)送指標(biāo)值
client.PostMetricValue(42)
}
單元測(cè)試
現(xiàn)在我們可以編寫(xiě)一個(gè)使用“偽造”Transport的單元測(cè)試:
// TestPostMetricValue確保客戶(hù)端向REST API發(fā)送正確的POST請(qǐng)求。
func TestPostMetricValue(t *testing.T) {
transport := &fakeTransport{}
client := NewClient(transport)
client.PostMetricValue(42)
if len(transport.calls) != 1 {
t.Fatal("期望1次傳輸調(diào)用,但是是%d次", len(transport.calls))
}
if transport.calls[0] != "POST https://monibot.io/api/metric, body=\\"value=42\\"" {
t.Fatal("錯(cuò)誤的傳輸調(diào)用 %q", transport.calls[0])
}
}
// 偽造的Transport是單元測(cè)試中使用的Transport。
type fakeTransport struct {
calls []string
}
func (f *fakeTransport) Post(url string, body []byte) {
f.calls = append(f.calls, fmt.Sprintf("POST %v, body=%q", url, string(body)))
}
添加更多的Transport函數(shù)
現(xiàn)在假設(shè)我們庫(kù)的其他部分,也使用了Transport功能,需要比POST更多的HTTP方法。對(duì)于它們,我們必須擴(kuò)展我們的Transport接口:
package monibot
// Transport傳輸請(qǐng)求。
type Transport interface {
Get(url string) []byte // 添加,因?yàn)閔ealth-monitor需要
Post(url string, body []byte)
Delete(url string) // 添加,因?yàn)閞esource-monitor需要
}
現(xiàn)在我們有一個(gè)問(wèn)題。編譯器抱怨我們的fakeTransport不再滿足Transport接口。所以讓我們通過(guò)添加缺失的函數(shù)來(lái)解決它:
// 偽造的Transport是單元測(cè)試中使用的Transport。
type fakeTransport struct {
calls []string
}
func (f *fakeTransport) Get(url string) []byte {
panic("不使用")
}
func (f *fakeTransport) Post(url string, body []byte) {
f.calls = append(f.calls, fmt.Sprintf("POST %v, body=%q", url, string(body)))
}
func (f *fakeTransport) Delete(url string) {
panic("不使用")
}
我們做了什么?由于在單元測(cè)試中我們不需要新的Get()和Delete()函數(shù),如果它們被調(diào)用,我們就拋出異常。這里有一個(gè)問(wèn)題:每次在Transport中添加新函數(shù)時(shí),我們都會(huì)破壞現(xiàn)有的fakeTransport實(shí)現(xiàn)。對(duì)于大型代碼庫(kù)來(lái)說(shuō),這將導(dǎo)致維護(hù)噩夢(mèng)。我們能做得更好嗎?
控制反轉(zhuǎn)
問(wèn)題在于我們的客戶(hù)端(和相應(yīng)的單元測(cè)試)依賴(lài)于一個(gè)它們不能控制的類(lèi)型。在這種情況下,它是Transport接口。為了解決這個(gè)問(wèn)題,讓我們通過(guò)引入一個(gè)未導(dǎo)出的接口,該接口僅聲明了我們的客戶(hù)端所需的內(nèi)容,來(lái)反轉(zhuǎn)控制:
package monibot
// clientTransport傳輸Client的請(qǐng)求。
type clientTransport interface {
Post(url string, body []byte)
}
type Client struct {
transport clientTransport
}
func NewClient(transport clientTransport) *Client {
return &Client{transport}
}
func (c *Client) PostMetricValue(value int) {
body := fmt.Sprintf("value=%d", value)
c.transport.Post("https://monibot.io/api/metric", []byte(body))
}
現(xiàn)在讓我們將我們的單元測(cè)試更改為使用假的clientTransport:
// TestPostMetricValue確保客戶(hù)端向REST API發(fā)送正確的POST請(qǐng)求。
func TestPostMetricValue(t *testing.T) {
transport := &fakeTransport{}
client := NewClient(transport)
client.PostMetricValue(42)
if len(f.calls) != 1 {
t.Fatal("期望1次傳輸調(diào)用,但是是%d次", len(f.calls))
}
if f.calls[0] != "POST https://monibot.io/api/metric, body=\\"value=42\\"" {
t.Fatal("錯(cuò)誤的傳輸調(diào)用 %q", f.calls[0])
}
}
// 偽造的Transport是在單元測(cè)試中使用的clientTransport。
type fakeTransport struct {
calls []string
}
func (f *fakeTransport) Post(url string, body []byte) {
f.calls = append(f.calls, fmt.Sprintf("POST %v, body=%q", url, string(body)))
}
由于Go的隱式接口實(shí)現(xiàn)(如果愿意,可以稱(chēng)之為'鴨子類(lèi)型'),我們庫(kù)的用戶(hù)什么也不需要改變:
import "monibot"
func main() {
// 初始化API客戶(hù)端
var transport monibot.HTTPTransport
client := monibot.NewClient(transport)
// 發(fā)送指標(biāo)值
client.PostMetricValue(42)
}
重新審視Transport
如果我們使IoC成為規(guī)范(正如我們應(yīng)該做的那樣),就不再需要導(dǎo)出Transport接口了。為什么呢?因?yàn)槿绻M(fèi)者需要一個(gè)接口,讓他們?cè)谧约旱淖饔糜蛑卸x它,就像我們對(duì)'clientTransport'做的那樣。
不要導(dǎo)出接口。導(dǎo)出具體實(shí)現(xiàn)。如果消費(fèi)者需要接口,讓他們?cè)谧约旱淖饔糜蛑卸x。
總結(jié)
在這篇文章中,我展示了如何以及為什么在Go中使用DI和IoC。正確使用DI/IoC可以導(dǎo)致更易于測(cè)試和維護(hù)的代碼,特別是在代碼庫(kù)不斷增長(zhǎng)時(shí)。雖然代碼示例是用Go編寫(xiě)的,但這里描述的原則同樣適用于其他編程語(yǔ)言。