函數(shù)式編程與設(shè)計(jì)模式(Cake 模式依賴注入)

在本篇中,我們會(huì)介紹 Scala 中如何使用 Cake 模式實(shí)現(xiàn)與傳統(tǒng)的依賴注入框架相同的功能。該模式使用了 Scala 的特質(zhì)和自身類型標(biāo)注(self-type annotation),從而在無需容器的情況下實(shí)現(xiàn)了與依賴注入提供的相同的組成和結(jié)構(gòu)。
我們將先回顧一下依賴注入的概念,然后再介紹 Cake 模式。
1 依賴注入
依賴注入,即 Dependency Injection,簡(jiǎn)稱 DI。想必熟悉 Spring 的朋友們應(yīng)該都見過這個(gè)概念,畢竟 Spring 面試題繞不過的控制反轉(zhuǎn)(Inverse of Control, IoC)就和依賴注入密切相關(guān)。兩者的關(guān)系,大致可以理解為通過依賴注入的手段實(shí)現(xiàn)了控制反轉(zhuǎn)。
依賴注入的目的就是:采用外部的配置或代碼來組合對(duì)象,而不是讓對(duì)象自行初始化其依賴——這樣做讓我們對(duì)象注入不同的依賴實(shí)現(xiàn)變得非常簡(jiǎn)單,并為我們理解給定對(duì)象有哪些依賴提供了一個(gè)集中的管理場(chǎng)所。
簡(jiǎn)單理解,那就是將軟件中某一接口具體實(shí)現(xiàn)類的選擇控制權(quán)從調(diào)用對(duì)象中轉(zhuǎn)移出來,交給外部第三方的配置來決定。對(duì)于 Spring 來說,就是交給了 Spring 容器的 Bean 配置(可以是 XML,也可以是純注解的配置類)來進(jìn)行控制。這個(gè)目的,就是我們常常說的“控制反轉(zhuǎn)”。
依賴注入本身則是由軟件界泰斗級(jí)人物 Martin Fowler (也是《重構(gòu)》一書作者)提出的概念,用以代替 IoC。依賴注入即讓調(diào)用類對(duì)某一接口實(shí)現(xiàn)類的依賴關(guān)系由第三方(容器或協(xié)作類)注入,以移除調(diào)用類對(duì)某一接口實(shí)現(xiàn)類的依賴。
這樣,通過依賴注入,也就使得調(diào)用類和接口實(shí)現(xiàn)類之間實(shí)現(xiàn)了解耦。這使得替換給定依賴的實(shí)現(xiàn)變得更簡(jiǎn)單。尤其是因此在單元測(cè)試中允許使用 Stub 或 Mock 實(shí)現(xiàn)來替代實(shí)際的依賴,使得代碼更容易被測(cè)試。
依賴注入主要就是通過構(gòu)造器或 setter 方法將依賴注入對(duì)象。
順便吐槽一下,網(wǎng)上有各種錯(cuò)誤的關(guān)于 Spring Bean 依賴注入方式有多少種的回答,數(shù)字可以從三種開始一直往上加……
但其實(shí)就只有我們提到的:構(gòu)造器和 setter 兩種。具體可以參考 Spring 官方文檔:1.4.1. Dependency Injection: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators 文檔中可以看到:“DI exists in two major variants: Constructor-based dependency injection and Setter-based dependency injection.”,即“DI 存在兩種:基于構(gòu)造函數(shù)的依賴注入和基于 Setter 的依賴注入?!?/p>
網(wǎng)上大多數(shù)錯(cuò)誤的文章是把 Bean 相關(guān)的一些 Spring 使用方式單獨(dú)作為一種來和這兩種注入方式并列,個(gè)人認(rèn)為其實(shí)并不合適。一種錯(cuò)誤是把 xml、注解實(shí)現(xiàn)這些具體的配置寫法作為單獨(dú)的一種分離出來,這種錯(cuò)誤較為明顯。還有一種錯(cuò)誤是誤把
@Autowired
或工廠方式作為單獨(dú)的種類,其實(shí)前者是把自動(dòng)裝配功能錯(cuò)誤理解成了注入方式的一種,而后者是將 Bean 的生成與注入本身混淆了。個(gè)人認(rèn)為以官方文檔為準(zhǔn)即可,分享一下相關(guān)思考供大家參考。
代碼示例
使用代碼舉個(gè)例子會(huì)更加清晰。下面的類粗略地描述了一個(gè)電影服務(wù),該服務(wù)可以返回用戶收藏的電影。它依賴于一個(gè)收藏服務(wù)和一個(gè)電影的 DAO(數(shù)據(jù)訪問對(duì)象),前者用來獲取收藏的電影列表,后者可以獲取每部電影的詳情:
此處使用的是傳統(tǒng)的基于構(gòu)造器的依賴注入。當(dāng)該服務(wù)被創(chuàng)建時(shí),MovieService 類必須將它依賴的內(nèi)容以入?yún)⒌姆绞絺魅霕?gòu)造器。這項(xiàng)工作可以手動(dòng)完成,但是通常我們會(huì)選擇一個(gè)依賴注入框架來替我們完成工作。
當(dāng)我們以更具函數(shù)式的風(fēng)格進(jìn)行編程時(shí),對(duì)依賴注入模式的需求將不會(huì)這么強(qiáng)烈。函數(shù)式編程本身就包含對(duì)函數(shù)的組合能力,由于這里所設(shè)計(jì)的函數(shù)組合能力與依賴注入對(duì)類的組合能力非常類似,所以我們無須任何代價(jià)就可以直接從函數(shù)組合中獲得與依賴注入相似的好處。
然而,簡(jiǎn)單的函數(shù)組合并不能解決所有依賴注入所能處理的問題。由于 Scala 是一門混血語(yǔ)言,所以這一點(diǎn)尤為突出。較大的代碼段通常會(huì)被組織為對(duì)象。
那么我們一起來看看依賴注入在 Scala 中的實(shí)現(xiàn)。
2 Scala 中實(shí)現(xiàn)依賴注入
傳統(tǒng)的依賴注入自然也可以在 Scala 中使用,甚至可以使用像 Spring 或 Guice 這樣為業(yè)界所熟知的 Java 框架。然而,在不使用任何框架的情況下也能達(dá)到許多同樣的目標(biāo)。這就是我們文章開頭提到的使用了 Scala 的特質(zhì)和自身類型標(biāo)注(self-type annotation)實(shí)現(xiàn)的 Cake 模式。
其大致思想就是將需要注入到頂層特質(zhì)的依賴進(jìn)行封裝,它們代表了可注入的組件。我們不會(huì)在特質(zhì)的內(nèi)部直接初始化依賴,而是在對(duì)它們進(jìn)行裝配的時(shí)候,創(chuàng)建抽象的 val 字段來保持對(duì)它們的引用。
然后,我們將會(huì)利用 Scala 的自身類型標(biāo)注和混入繼承,以一種類型安全的方式來指定裝配。最后,采用一個(gè)簡(jiǎn)單的 Scala 對(duì)象作為我們的組件注冊(cè)表。我們會(huì)將所有的依賴混入到該容器對(duì)象中并對(duì)它們進(jìn)行初始化,然后采用前面提到的抽象 val 字段來保持對(duì)它們的引用。
這種方式具備了一些不錯(cuò)的特性。正如前面所提到的,它并不需要使用外部的容器。此外,在將事物裝配起來的同時(shí)還維持了靜態(tài)類型的安全。
讓我們介紹一下即將在示例代碼中出現(xiàn)的數(shù)據(jù):我們擁有三個(gè)樣本類——Movie
、Video
和 DecoratedMovie
(配備了視頻的電影)。
現(xiàn)在定義一些特質(zhì)作為接口,并將它們作為依賴:FavoritesService
和 MovieDao
。我們會(huì)將這些特質(zhì)嵌入到另一組用于表示可注入組件的特質(zhì)中。
接下來,完成對(duì)剛才所引入的組件的實(shí)現(xiàn)。我們將通過實(shí)現(xiàn)這些接口來為 MovieDao
和 FavoritesService
生成測(cè)試 Stub,從而返回靜態(tài)的響應(yīng)。請(qǐng)注意,此外還需要對(duì)這些包裝進(jìn)來的組件特質(zhì)進(jìn)行擴(kuò)展。
現(xiàn)在來看看 MovieServiceImpl
,它依賴于前面定義的 FavoritesService
和 MovieDao
。該類就實(shí)現(xiàn)了一個(gè)方法:getFavoriteDecoratedMovies()
,該方法以用戶 ID 為入?yún)?,并返回用戶收藏的電影,這些電影都配備了相關(guān)聯(lián)的視頻。
MovieServiceImpl
的全部代碼包裝在一個(gè)頂層的 MovieServiceComponentImpl
特質(zhì)中,如下所示:
這里首行的 this: MovieDaoComponent with FavoritesServiceComponent =>
就是所說的自身類型標(biāo)注,它可以確保 Cake 模式的類型安全。該自身類型標(biāo)注確保了不管在何時(shí),只要某個(gè)對(duì)象或類混入了 MovieServiceComponentImpl
,那么該對(duì)象的引用都會(huì)擁有類型 MovieDaoComponent with FavoritesServiceComponent
。換句話說,它確保了每當(dāng)某些對(duì)象或類混入 MovieServiceComponentImpl
時(shí),MovieDaoComponent
和 FavoritesServiceComponent
抑或是它們的子類也都將會(huì)混入該對(duì)象或類。
維持靜態(tài)類型的安全,意味著如果下文中的對(duì)象注冊(cè)表
ComponentRegistry
只擴(kuò)展MovieServiceComponentImpl
的話,會(huì)導(dǎo)致編譯器錯(cuò)誤。編譯器會(huì)告訴我們,該注冊(cè)表不符合我們?yōu)?MovieServiceComponentImpl
所聲明的自身類型,即我們沒有為其混入MovieDaoComponent
和FavoritesServiceComponent
。
接下來聲明的顯式 val 字段存儲(chǔ)了依賴的引用,這些能確保每當(dāng)我們將 MovieServiceComponentImpl
混入容器對(duì)象時(shí),都需要將它們分配給抽象的 val 字段。
這里確保分配 val 字段的對(duì)象是指:如果下文中的注冊(cè)表
ComponentRegistry
沒有實(shí)現(xiàn)成員favoritesService
和movieDao
,會(huì)編譯報(bào)錯(cuò)。
最后,創(chuàng)建作為組件注冊(cè)表的對(duì)象:ComponentRegistry
。該注冊(cè)表擴(kuò)展了我們所有的依賴實(shí)現(xiàn),并對(duì)它們進(jìn)行了初始化,然后將它們的引用存儲(chǔ)在前面定義的 val 字段中:
完成以上代碼后,我們就可以在使用場(chǎng)景下從注冊(cè)表中獲取裝配完整的 MovieService
了:
如果需要在單元測(cè)試中注入依賴的測(cè)試 Stub,則可以按照如下方式操作(可以看出,代碼是類似的,只是實(shí)現(xiàn)變換了):
由于 Cake 模式使用了 Scala 的自身類型標(biāo)注,Java 中無法復(fù)刻該模式。
之前文章
函數(shù)式編程與設(shè)計(jì)模式(其他 GoF 設(shè)計(jì)模式)