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