Facebook 對(duì) Golang 依賴注入的實(shí)現(xiàn)
依賴注入是一個(gè)經(jīng)典的設(shè)計(jì)模式,在解決復(fù)雜的對(duì)象依賴關(guān)系方面是一個(gè)非常行之有效的手段。
對(duì)于有反射功能的語言來說,實(shí)現(xiàn)依賴注入都比較方便一些。在 Golang 中有幾個(gè)比較知名的依賴注入開源庫,例如 google/wire、uber-go/dig 以及 facebookgo/inject 等。
本文將基于 facebookgo/inject 介紹依賴注入, 接下來將會(huì)著重討論以下幾點(diǎn)內(nèi)容:
依賴注入的背景以及解決的問題 facebookgo/inject 的使用方法 facebookgo/inject 的缺陷
依賴注入的背景
對(duì)于稍微復(fù)雜些的項(xiàng)目,我們往往就會(huì)遇到對(duì)象之間復(fù)雜的依賴關(guān)系。手動(dòng)管理和初始化這些管理關(guān)系將會(huì)極其繁瑣,依賴注入可以幫我們自動(dòng)實(shí)現(xiàn)依賴的管理和對(duì)象屬性的賦值,將我們從這些繁瑣的依賴管理中解放出來。
以一個(gè)常見的 HTTP 服務(wù)為例,我們?cè)陂_發(fā)后臺(tái)時(shí)往往會(huì)把代碼分為 Controller、Service 等層次。如下:
type?UserController?struct?{
?UserService?*UserService
?Conf????????*Conf
}
type?PostController?struct?{
?UserService?*UserService
?PostService?*PostService
?Conf????????*Conf
}
type?UserService?struct?{
?Db???*DB
?Conf?*Conf
}
type?PostService?struct?{
?Db?*DB
}
type?Server?struct?{
????UserApi?*UserController
????PostApi?*PostController
}
上述的代碼例子中,有兩個(gè) Controller:UserController 和 PostController,分別用來接收用戶和文章的相關(guān)請(qǐng)求邏輯。除此之外還會(huì)有 Service 相關(guān)類、Conf 配置文件、DB 連接等。
這些對(duì)象之間存在比較復(fù)雜的依賴關(guān)系,這就給項(xiàng)目的初始化帶來了一些困擾。對(duì)于以上代碼,對(duì)應(yīng)初始化邏輯大概就會(huì)是這樣:
func?main()?{
?conf?:=?loadConf()
?db?:=?connectDB()
?userService?:=?&UserService{
??Db:???db,
??Conf:?conf,
?}
?postService?:=?&PostService{
??Db:?db,
?}
?userHandler?:=?&UserController{
??UserService:?userService,
??Conf:????????conf,
?}
?postHandler?:=?&PostController{
??UserService:?userService,
??PostService:?postService,
??Conf:????????conf,
?}
?server?:=?&Server{
??UserApi:?userHandler,
??PostApi:?postHandler,
?}
?server.Run()
}
我們會(huì)有一大段的邏輯都是用來做對(duì)象初始化,而當(dāng)接口越來越多的時(shí)候,整個(gè)初始化過程就會(huì)異常的冗長(zhǎng)和復(fù)雜。
針對(duì)以上問題,依賴注入可以完美的解決。
facebookgo/inject 的使用
接下來,我們?cè)囍褂?facebookgo/inject 的方式,對(duì)這段代碼進(jìn)行依賴注入的改造。如下:
type?UserController?struct?{
?UserService?*UserService?`inject:""`
?Conf????????*Conf????????`inject:""`
}
type?PostController?struct?{
?UserService?*UserService?`inject:""`
?PostService?*PostService?`inject:""`
?Conf????????*Conf????????`inject:""`
}
type?UserService?struct?{
?Db???*DB???`inject:""`
?Conf?*Conf?`inject:""`
}
type?PostService?struct?{
?Db?*DB?`inject:""`
}
type?Server?struct?{
?UserApi?*UserController?`inject:""`
?PostApi?*PostController?`inject:""`
}
func?main()?{
?conf?:=?loadConf()?//?*Conf
?db?:=?connectDB()?//?*DB
?server?:=?Server{}
?graph?:=?inject.Graph{}
?if?err?:=?graph.Provide(
??&inject.Object{
???Value:?&server,
??},
??&inject.Object{
???Value:?conf,
??},
??&inject.Object{
???Value:?db,
??},
?);?err?!=?nil?{
??panic(err)
?}
?if?err?:=?graph.Populate();?err?!=?nil?{
??panic(err)
?}
?server.Run()
}
首先每一個(gè)需要注入的字段都需要打上
inject:""這樣的 tag。所謂依賴注入,這里的依賴指的就是對(duì)象中包含的字段,而注入則是指有其它程序會(huì)幫你對(duì)這些字段進(jìn)行賦值。其次,我們使用
inject.Graph{}創(chuàng)建一個(gè) graph 對(duì)象。這個(gè) graph 對(duì)象將負(fù)責(zé)管理和注入所有的對(duì)象。至于為什么叫 Graph,其實(shí)這個(gè)名詞起的非常形象,因?yàn)楦鱾€(gè)對(duì)象之間的依賴關(guān)系,也確實(shí)像是一張圖一樣。接下來,我們使用
graph.Provide()將需要注入的對(duì)象提供給graph。
graph.Provide(
?&inject.Object{
??Value:?&server,
?},
?&inject.Object{
??Value:?&conf,
?},
?&inject.Object{
??Value:?&db,
?},
);
最后調(diào)用 Populate函數(shù),開始進(jìn)行注入。
從代碼中可以看到,我們一共就向 Graph 中 Provide 了三個(gè)對(duì)象。我們提供了 server 對(duì)象,是因?yàn)樗且粋€(gè)頂層對(duì)象。提供了 conf 和 db對(duì)象,是因?yàn)樗械膶?duì)象都依賴于它們,可以說它們是基礎(chǔ)對(duì)象了。
但是其他的對(duì)象呢?例如 UserApi 和 UserService 呢?我們并沒有向 graph 調(diào)用 Provide 過。那么它們是怎么完成賦值和注入的呢?
其實(shí)從下面這張對(duì)象依賴圖能夠很簡(jiǎn)單的看清楚。

從這個(gè)依賴圖中可以看出,conf 和 db 對(duì)象是屬于根節(jié)點(diǎn),所有的對(duì)象都依賴和包含著它們。而 server 屬于葉子節(jié)點(diǎn),不會(huì)有其他對(duì)象依賴它了。
我們需要提供給 Graph 的就是根節(jié)點(diǎn)和葉子節(jié)點(diǎn),對(duì)于中間節(jié)點(diǎn)來說,Graph 會(huì)通過 inject:"" 標(biāo)簽,自動(dòng)將其 Provide 到 Graph 中,并進(jìn)行注入。
對(duì)以上例子,我們深入剖析下 Graph 內(nèi)部進(jìn)行 Populate 時(shí)都發(fā)生了哪些動(dòng)作:
Graph 首先解析 server 對(duì)象,發(fā)現(xiàn)其有兩個(gè)標(biāo)記為 inject 的字段: UserApi和PostApi。其類型UserController和PostController, Graph 中從未出現(xiàn)過這兩個(gè)類型。因此,Graph 會(huì)自動(dòng)對(duì)該字段調(diào)用 Provide,提供給 Graph。解析 UserApi 時(shí),發(fā)現(xiàn)其依然有也有兩個(gè)標(biāo)記為 inject 的字段: UserService和Conf。對(duì)于UserService這種 Graph 中未登記過的類型,會(huì)自動(dòng) Provide。而對(duì)Conf, Graph 中之前已經(jīng)注冊(cè)過了,因此直接將注冊(cè)的對(duì)象賦值給該字段即可。接下來就是繼續(xù)逐步解析,直至沒有tag為 inject 的字段。
以上就是整個(gè)依賴注入的流程了。
這里需要注意的是,在我們上面的示例中,以這種方式注入,其中所有的對(duì)象都相當(dāng)于單例對(duì)象。即一個(gè)類型,只會(huì)在 Graph 中存在一個(gè)實(shí)例對(duì)象。比如 UserController 和 PosterController 中的 UserService 實(shí)際上是同一個(gè)對(duì)象。
我們的 main 函數(shù)使用 inject 進(jìn)行改造后,將會(huì)變得非常簡(jiǎn)潔。而且即使隨著業(yè)務(wù)越來越復(fù)雜,Handler 和 Service 越來越多,這個(gè) main 函數(shù)中的注入邏輯也不會(huì)任何改變,除非有新的根節(jié)點(diǎn)對(duì)象出現(xiàn)。
當(dāng)然,對(duì)于 Graph 來說,也不是只能 Provide 根節(jié)點(diǎn)和葉子節(jié)點(diǎn),我們也可以自行 Provide 一個(gè) UserService 的實(shí)例進(jìn)去,對(duì)于 Graph 的運(yùn)作是沒有任何影響的。只不過只 Provide 根節(jié)點(diǎn)和葉子節(jié)點(diǎn),代碼會(huì)更簡(jiǎn)潔一些。
inject 的高級(jí)用法
我們?cè)诼暶?tag 時(shí),除了聲明為 inject:"" 這種默認(rèn)用法外,還可以有其他三種高級(jí)的用法:
inject:"private"。私有注入。inject:"inline"。內(nèi)聯(lián)注入。inject:"object_name"。命名注入,這里的 object_name 可以取成任意的名字。
private (私有注入)
我們上文講過,默認(rèn)情況下,所有的對(duì)象都是單例對(duì)象。一個(gè)類型只會(huì)有一個(gè)實(shí)例對(duì)象存在。但也可以不使用單例對(duì)象,private 就是提供了這種可能。
例如:
type?UserController?struct?{
?UserService?*UserService?`inject:"private"`
?Conf????????*Conf????????`inject:""`
}
我們將 UserController 中的 UserService 屬性聲明為 private 注入。這樣的話,graph 遇到 private 標(biāo)簽時(shí),會(huì)自動(dòng)的 new 一個(gè)全新的 UserService 對(duì)象,將其賦值給該字段。
這樣 Graph 中就同時(shí)存在了兩個(gè) UserService 的實(shí)例,一個(gè)是 UserService 的全局實(shí)例,給默認(rèn)的 inject:"" 使用。一個(gè)是專門給 UserController 實(shí)例中的 UserService 使用。
但在實(shí)際開發(fā)中,這種 private 的場(chǎng)景似乎也比較少,大部分情況下,默認(rèn)的單例對(duì)象就足夠了。
inline (內(nèi)聯(lián)注入)
默認(rèn)情況下,需要注入的屬性必須得是 *Struct。但是也是可以聲明為普通對(duì)象的。例如:
type?UserController?struct?{
?UserService?UserService?`inject:"inline"`
?Conf????????*Conf???????`inject:""`
}
注意,這里的 UserService 的類型,并非是 *UserService 指針類型了,而是普通的 struct 類型。struct 類型在 Go 里面都是值語義,這里當(dāng)然也就不存在單例的問題了。
命名注入
如果我們需要對(duì)某些字段注入專有的對(duì)象實(shí)例,那么我們可能會(huì)用到命名注入。使用方法就是在 inject 的 tag 里寫上專有的名字。如下:
type?UserController?struct?{
?UserService?UserService?`inject:"named_service"`
?Conf????????*Conf???????`inject:""`
}
當(dāng)然,這個(gè)命名肯定不能命名為 private 和 inline,這兩個(gè)屬于保留詞。
同時(shí),我們一定要把這個(gè)命名實(shí)例 Provide 到 graph 里面,這樣 graph 才能把兩個(gè)對(duì)象聯(lián)系起來。
graph.Provide(
?&inject.Object{
??Value:?&namedService,
??Name:?"named_service",
?},
);
注入 map
我們除了可以注入對(duì)象外,還可以注入 map。如下:
type?UserController?struct?{
?UserService?UserService???????`inject:"inline"`
?Conf????????*Conf?????????????`inject:""`
?UserMap?????map[string]string?`inject:"private"`
}
需要注意的是,map 的注入 tag 一定要是 inject:"private"。
facebookgo/inject 的缺陷
facebookgo/inject 固然很好用,只要聲明 inject:"" 的 tag,提供幾個(gè)對(duì)象,就可以完全自動(dòng)的注入所有依賴關(guān)系。
但是由于Golang本身的語言設(shè)計(jì), facebookgo/inject 也會(huì)有一些缺陷和短板:
所有需要注入的字段都需要是 public 的。 這也是 Golang 的限制,不能對(duì)私有屬性進(jìn)行賦值。所以只能對(duì)public的字段進(jìn)行注入。但這樣就會(huì)把代碼稍顯的不那么優(yōu)雅,畢竟很多變量我們其實(shí)并不想 public。
只能進(jìn)行屬性賦值,不能執(zhí)行初始化函數(shù)。 facebookgo/inject只會(huì)幫你注入好對(duì)象,把各個(gè)屬性賦值好。但很多時(shí)候,我們往往需要在對(duì)象賦值完成后,再進(jìn)行其他一些動(dòng)作。但對(duì)于這個(gè)需求場(chǎng)景,facebookgo/inject并不能很好的支持。
這兩個(gè)問題的原因總結(jié)歸納為:Golang沒有構(gòu)造函數(shù)......
推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛好者值得關(guān)注
