Dependency Injection 這個(gè)名詞,是在 Martin Fowler 的《Inversion of Control Containers and the Dependency Injection pattern》文章之后才廣為人知。在文章中,Martin 解釋了當(dāng)時(shí)初起流行的 IOC 概念:為了消除應(yīng)用程序?qū)Σ寮?shí)現(xiàn)的依賴,程序的主控權(quán)從應(yīng)用程序移到了框架。為了讓 IOC 概念不那么令人迷惑,Martin 把流行的幾種 IOC Container 實(shí)現(xiàn)模式命名為 Dependency Injection Pattern(DIP,下文簡稱 DI 模式)。很明顯,DI 的定義更準(zhǔn)確形象,而且因?yàn)?Martin 在軟件開發(fā)社區(qū)巨大的影響力,DI 模式以及 spring framework 作為其最成功的實(shí)現(xiàn)之一很快被軟件開發(fā)社區(qū)接受并成為日常開發(fā)的必備利器。
“依賴注入”給我們代碼的編寫,特別是測試代碼的編寫帶來了很多好處。借助于 DI 模式,我們可以將服務(wù)的依賴聲明和具體實(shí)現(xiàn)相分離,通過配置不同的服務(wù)實(shí)例交給框架管理,來讓應(yīng)用程序獲得良好的靈活性和柔性。
比如我們有這樣的代碼:
public void hello() {
Foo foo = new Foo();
foo.sayHello();
//
}
這種情況下,hello() 方法就依賴于 Foo 類的 sayHello() 方法實(shí)現(xiàn),會(huì)給測試帶來了很多的不穩(wěn)定性,比如耗時(shí)、服務(wù)異常之類。如果依照 DI 模式來重構(gòu),這段代碼就會(huì)變成這樣:
public void hello(Foo foo) {
foo.sayHello();
//
}
在測試時(shí)我們就可以使用行為確定符合預(yù)期的‘mock’的 Foo 類來代替實(shí)際的 Foo 類,從而將測試關(guān)注點(diǎn)聚集到 hello() 方法,也避免了 Foo 類 sayHello() 方法的具體實(shí)現(xiàn)對測試可能帶來的影響。
以上就是 DIP 最常見也是最令人熟悉的一層涵義,從 IOC、spring framework 開始接受 DIP,自然而然會(huì)把 DIP 等同于 DI 模式,但其實(shí)它還有另一層涵義。在《Agile Software Developement:principles,Patterns,and Practices》的第11章 依賴倒置原則,給出了 DIP 的另一種解讀:DIP —— 依賴倒置原則,這里的 DIP 就不再是 Dependency Injection Pattern(依賴注入模式),而是 Dependency Inversion Principle(依賴倒置原則,以下簡稱 DI 原則)。DI 原則主要包括下面兩條啟發(fā)式規(guī)則:
A. 高層模塊不應(yīng)依賴于低層模塊。二者都應(yīng)依賴于抽象
B. 抽象不應(yīng)依賴于細(xì)節(jié)。細(xì)節(jié)應(yīng)依賴于抽象
人們通常會(huì)用好萊塢法則來詮釋啟發(fā)規(guī)則 A:“Don't call us, we'll call you.”其中的 we/us,就是指高層模塊,you 則是低層模塊,高層模塊來決定低層的模塊,就像好萊塢制片人最終掌握著演員上鏡與否的生殺大權(quán)。
我們來看看軟件系統(tǒng)中常見的類關(guān)系,高層的 Policy Layer 使用了低層的 Mechanism Layer,而低層的 Mechanism Layer 又使用了更低層的 Utility Layer(見圖1)
圖1中的依賴關(guān)系是傳遞的:高層的 Policy Layer 對于其下一直到 Utility Layer 的改動(dòng)都是敏感的,一旦我們修改了低層模塊的實(shí)現(xiàn),高層模塊不得不也修改相應(yīng)的實(shí)現(xiàn)來適應(yīng)低層模塊的修改。這種依賴關(guān)系,與軟件復(fù)用的目標(biāo)是相悖的,而且也不符合實(shí)際的業(yè)務(wù)變化。我們通常會(huì)需要切換低層的實(shí)現(xiàn)方式或者版本,而很少會(huì)去修改高層的業(yè)務(wù)。比如有一個(gè)證書申請發(fā)放系統(tǒng),需要使用異步的消息機(jī)制來處理用戶請求。客戶可能會(huì)要求把底層的 MessageQueue 從 IIS 切換成 ActiveQ,但高層的證書申請發(fā)放的業(yè)務(wù)流程是穩(wěn)定的,不會(huì)因低層基礎(chǔ)服務(wù)的改變而作出修改。
所以,為了應(yīng)付現(xiàn)實(shí)中的變化,也為了向高層屏蔽這些變化,我們通常會(huì)給低層模塊抽象出接口,作為高層和低層之間的契約。這樣,高層模塊就應(yīng)該只與低層模塊的接口打交道,不再關(guān)心低層模塊的實(shí)現(xiàn)細(xì)節(jié)。而低層模塊的實(shí)現(xiàn),我們可以通過配置文件由框架來切換,不影響到高層的行為。說到這里,大家應(yīng)該可以看到 Spring 的影子了。此時(shí)類關(guān)系圖就變成了下圖(圖2)
嗯,現(xiàn)在的類關(guān)系已經(jīng)遵循接口依賴了,甚至加上了框架的 DI 模式,系統(tǒng)也具有了一定的靈活性和易改變性,看起來很像大多數(shù)的系統(tǒng)了。但是,這是不是就是符合 DI 原則呢?我們可以看到,這里的接口通常是由低層模塊來定義和派生,也就是低層模塊抽象出來提供給外界調(diào)用的服務(wù)接口。高層模塊其實(shí)還是依賴于低層模塊,只是這次高層模塊產(chǎn)生依賴的是低層模塊里面聲明的接口。想起曾經(jīng)在 javaeye 上看到一篇帖子,作者抱怨為什么 java 不提供 extracts 關(guān)鍵字,這樣就可以由框架或者容器在具體類上抽出接口定義(與 implements 對應(yīng)),省得手工創(chuàng)建這些接口?;蛟S這是目前依賴注入框架帶來的誤區(qū)吧:使用人員不理解 DIP 的本意,單純是為框架的約束而創(chuàng)建。此時(shí)聲明的類關(guān)系顯然還是沒有完全體現(xiàn) DI 原則的精髓。
在書中 Robert 進(jìn)一步對 DI 原則給出了解釋:“請注意這里的倒置不僅僅是依賴關(guān)系的倒置,也是接口所有權(quán)的倒置。我們通常會(huì)認(rèn)為工具庫應(yīng)該擁有它們自己的接口,但是應(yīng)用 DIP (DI 原則)時(shí),我們發(fā)現(xiàn)往往是消費(fèi)者擁有抽象接口,而它們的服務(wù)者則從這些接口派生。”基于這種思路,前面例子更符合面向?qū)ο笏枷氲膶哟侮P(guān)系圖如下(圖3)
圖3和圖2的區(qū)別主要在接口的所有權(quán):高層模塊應(yīng)該擁有接口所有權(quán),低層模塊派生自高層模塊里定義的接口。這就意味著:
1. 高層模塊引用的接口定義應(yīng)該和高層模塊的其他類放在一起
2. 高層模塊的復(fù)用是把高層擁有的所有類和接口定義作為一個(gè)整體來復(fù)用的
3. 接口定義的改變只有根據(jù)高層模塊的需要才進(jìn)行的,而不是低層模塊
OK,直到現(xiàn)在,我們才能說 DI 原則原來是這個(gè)意思,否則,體會(huì)不到就有可能誤人誤己。Robert 在本章的結(jié)論中說“事實(shí)上,這種依賴關(guān)系的倒置正是好的面向?qū)ο笤O(shè)計(jì)的標(biāo)志所在,使用何種語言來編寫程序是無關(guān)緊要的。如果程序的依賴關(guān)系是倒置的,它就是面向?qū)ο蟮脑O(shè)計(jì)。如果程序的依賴關(guān)系不是倒置的,它就是過程化的設(shè)計(jì)。”信哉此言!
那么,要按照 OOA & OOD 的設(shè)計(jì)思路來進(jìn)行系統(tǒng)設(shè)計(jì),DI 原則對我們有什么幫助呢?其實(shí),DI 原則不僅僅是抽象的原則,而且是可以啟發(fā)推導(dǎo)出出種種具體的實(shí)踐。我們來看看對 OOA & OOD 的幫助。因?yàn)橐蕾囮P(guān)系是倒置的,就可以通過對高層策略的抽象和定義驅(qū)動(dòng)出低層服務(wù)者的接口。以此類推,直到把最底層的模塊設(shè)計(jì)出為止。那什么是高層策略呢?怎么找出潛在的抽象?書中同樣給出了答案:“它是應(yīng)用背后的抽象,是那些不隨具體細(xì)節(jié)的改變而改變的真理。它是系統(tǒng)內(nèi)部的系統(tǒng)——它是隱喻(metaphore)”更有詳細(xì)的具體實(shí)踐,敬請關(guān)注本博其他文章。
References:
Inversion of Control Containers and the Dependency Injection pattern,http://www.martinfowler.com/articles/injection.html,Martin Fowler
控制反轉(zhuǎn)與依賴注入模式,http://gigix.blogdriver.com/diary/gigix/inc/DependencyInjection.pdf,熊節(jié) 譯
《Agile Software Developement:principles,Patterns,and Practices》,Robert Fowler 著,鄧輝 譯,孟巖 審