spring等輕量級容器能夠協助開發人員將來自不同項目的組件組裝成為一個內聚的應用程式.。在它們的背後有著同一個模式,這個模式決定了這些容器進行組件裝配的方式。人們用一個大而化之的名字來稱呼這個模式:“控制反轉”( Inversion ofControl,IoC).給它一個更能描述其特點的名字——“依賴注入”(Dependency Injection),並將其與“服務定位器”(Service Locator)模式作一個比較。不過,這兩者之間的差異並不太重要,更重要的是:應該將組件的配置與使用分離開——兩個模式的目標都是這個。(應用程式用組件組裝起來,但不相依元件的實作類別,將應用程式和組件的實現解耦合)
J2EE 開發人員常遇到的一個問題就是如何組裝不同的程式元素:如果web 控制器體繫結構和資料庫介面是由不同的團隊所開發的,彼此幾乎一無所知,你應該如何讓它們配合工作?很多架構嘗試過解決這個問題,有幾個架構索性朝這個方向發展,提供了更通用的“組裝各層組件”的方案。這樣的架構通常被稱為“輕量級容器”,PicoContainer 和Spring 都在此列中。
所謂“組件”是指這樣一個軟體單元:它將被作者無法控制的其他應用程式使用,但後者不能對
組件進行修改。也就是說,使用一個組件的應用程式不能修改組件的原始碼,但可以通過作者預
留的某種途徑對其進行擴充,以改變組件的行為。
服務和組件有某種相似之處:它們都將被外部的應用程式使用。在我看來,兩者之間最大的差異
在於:組件是在本地使用的(例如JAR 檔案、程式集、DLL、或者源碼匯入);而服務是要通過
——同步或非同步——遠程介面來遠程使用的(例如web service、訊息系統、RPC,或者
socket)。
在一個真實的系統中,我們可能有數十個服務和組件。在任何時候,我們
總可以對使用組件的情形加以抽象,通過介面與具體的組件交流(如果組件並沒有設計一個介面,
也可以通過適配器與之交流)。但是,如果我們希望以不同的方式部署這個系統,就需要用外掛程式
機制來處理服務之間的互動過程,這樣我們才可能在不同的部署方案中使用不同的實現。
所以,現在的核心問題就是:如何將這些外掛程式組合成一個應用程式?這正是新生的輕量級容器所
面臨的主要問題,而它們解決這個問題的手段無一例外地是控制反轉(Inversion of Control)
模式。
幾位輕量級容器的作者曾驕傲地對我說:這些容器非常有用,因為它們實現了“控制反轉”。這
樣的說辭讓我深感迷惑:控制反轉是架構所共有的特徵,如果僅僅因為使用了控制反轉就認為這
些輕量級容器與眾不同,就好象在說“我的轎車是與眾不同的,因為它有四個輪子”。
我想我們需要給這個模式起一個更能說明其特點的名字——“控制反轉”這個名字太泛了,
常常讓人有些迷惑。與多位IoC 愛好者討論之後,我們決定將這個模式叫做“依賴注入”
(Dependency Injection)。
依賴注入的形式主要有三種,我分別將它們叫做構造子注入(Constructor Injection)、設值
方法注入(Setter Injection)和介面注入(Interface Injection)
依賴注入的最大好處在於:它消除了應用程式類對具體組件實作類別的依賴。這樣
一來, 我就可以把應用程式 類交給朋友, 讓他們根據自己的環境插入一個合適的
組件實現即可。不過,Dependency Injection 模式並不是打破這層依賴關係的唯一
手段,另一種方法是使用Service Locator 模式。
Service Locator 模式背後的基本思想是:有一個對象(即服務定位器)知道如何獲得一個應用
程式所需的所有服務。也就是說,在我們的例子中,服務定位器應該有一個方法,用於獲得一個
MovieFinder 執行個體。在這裡,我把ServiceLocator 類實現為一個Singleton 的註冊表,於是應用程式就可以
在執行個體化時通過ServiceLocator 獲得一個組件的執行個體
Dependency Injection 和Service Locator 兩個模式並不是互斥的,你可以同時使用它們,
Avalon 架構就是這樣的一個例子。Avalon 使用了服務定位器,但“如何獲得定位器”的資訊
則是通過注入的方式告知組件的。
Service Locator vs. Dependency Injection
首先,我們面臨Service Locator 和Dependency Injection 之間的選擇。應該注意,儘管我
們前面那個簡單的例子不足以表現出來,實際上這兩個模式都提供了基本的解耦合能力——無論
使用哪個模式,應用程式代碼都不依賴於服務介面的具體實現。兩者之間最重要的區別在於:這
個“具體實現”以什麼方式提供給應用程式代碼。使用Service Locator 模式時,應用程式代
碼直接向服務定位器發送一個訊息,明確要求服務的實現;使用Dependency Injection 模式
時,應用程式代碼不發出顯式的請求,服務的實現自然會出現在應用程式代碼中,這也就是所謂
“控制反轉
控制反轉是架構的共同特徵,但它也要求你付出一定的代價:它會增加理解的難度,並且給調試
帶來一定的困難。所以,整體來說,除非必要,否則我會盡量避免使用它。這並不意味著控制反
轉不好,只是我認為在很多時候使用一個更為直觀的方案(例如Service Locator 模式)會比
較合適。
Dependency Injection 模式可以協助你看清組件之間的依賴關係:你只需觀察依賴注入的機
制(例如構造子),就可以掌握整個依賴關係。而使用Service Locator 模式時,你就必須在源
代碼中到處搜尋對服務定位器的調用。
一個關鍵的區別在於:使用Service Locator 模式時,服務的使用者必須依賴於服務定位器。
定位器可以隱藏使用者對服務具體實現的依賴,但你必須首先看到定位器本身。所以,問題的答
案就很明朗了:選擇Service Locator 還是Dependency Injection,取決於“對定位器的依
賴”是否會給你帶來麻煩。
人們傾向於使用Dependency Injection 模式的一個常見理由是:它簡化了測試工作。這裡的
關鍵是:出於測試的需要,你必須能夠輕鬆地在“真實的服務實現”與“供測試用的‘偽’組件”
之間切換。但是,如果單從這個角度來考慮,Dependency Injection 模式和Service Locator
模式其實並沒有太大區別:兩者都能夠很好地支援“偽”組件的插入。之所以很多人有
“Dependency Injection 模式更利於測試”的印象,我猜是因為他們並沒有努力保證服務定
位器的可替換性。這正是持續測試起作用的地方:如果你不能輕鬆地用一些“偽”組件將一個服
務架起來以便測試,這就意味著你的設計出現了嚴重的問題。
當然,如果組件環境具有非常強的侵略性(就像EJB 架構那樣),測試的問題會更加嚴重。我的
觀點是:應該盡量減少這類架構對應用程式代碼的影響,特別是不要做任何可能使“編輯-執行”
的迴圈變慢的事情。用外掛程式(plugin)機製取代重量級組件會對測試過程有很大協助,這正是
測試驅動開發(Test Driven Development,TDD)之類實踐的關鍵所在
構造子注入 vs. 設值方法注入
設值函數注入和構造子注入之間的選擇相當有趣,因為它折射出物件導向編程的一些更普遍的問
題:應該在哪裡填充對象的欄位,構造子還是設值方法?
一直以來,我首選的做法是盡量在構造階段就建立完整、合法的對象——也就是說,在構造子中
填充對象欄位。這樣做的好處可以追溯到Kent Beck 在Smalltalk Best Practice Patterns
一書中介紹的兩個模式:Constructor Method 和Constructor Parameter Method。帶有參
數的構造子可以明確地告訴你如何建立一個合法的對象。如果建立合法對象的方式不止一種,你
還可以提供多個構造子,以說明不同的組合方式。
構造子初始化的另一個好處是:你可以隱藏任何不可變的欄位——只要不為它提供設值方法就行
了。我認為這很重要:如果某個欄位是不應該被改變的,“沒有針對該欄位的設值方法”就很清
楚地說明了這一點。如果你通過設值方法完成初始化,暴露出來的設值方法很可能成為你心頭永
遠的痛
不過,世事總有例外。如果參數太多,構造子會顯得淩亂不堪,特別是對於不支援關鍵字參數的
語言更是如此。的確,如果構造子參數列表太長,通常標誌著對象太過繁忙,理應將其拆分成幾
個對象,但有些時候也確實需要那麼多的參數。
如果有不止一種的方式可以構造一個合法的對象,也很難通過構造子描述這一資訊,因為構造子
之間只能通過參數的個數和類型加以區分。這就是Factory Method 模式適用的場合了,工廠
方法可以藉助多個私人構造子和設值方法的組合來完成自己的任務。經典Factory Method 模
式的問題在於:它們往往以靜態方法的形式出現,你無法在介面中聲明它們。你可以建立一個工
廠類,但那又變成另一個服務實體了。“工廠服務”是一種不錯的技巧,但你仍然需要以某種方
式執行個體化這個工廠對象,問題仍然沒有解決。
如果要傳入的參數是像字串這樣的簡單類型,構造子注入也會帶來一些麻煩。使用設值方法注
入時,你可以在每個設值方法的名字中說明參數的用途;而使用構造子注入時,你只能靠參數的
位置來決定每個參數的作用,而記住參數的正確位置顯然要困難得多。
如果對象有多個構造子,對象之間又存在繼承關係,事情就會變得特別討厭。為了讓所有東西都
正確地初始化,你必須將對子類構造子的調用轉寄給超類的構造子,然後處理自己的參數。這可
能造成構造子規模的進一步膨脹。
儘管有這些缺陷,但我仍然建議你首先考慮構造子注入。不過,一旦前面提到的問題真的成了問
題,你就應該準備轉為使用設值方法注入。
代碼配置 vs. 設定檔
另一個問題相對獨立,但也經常與其他問題牽涉在一起:如何佈建服務的組裝,通過設定檔還
是直接編碼組裝?對於大多數需要在多處部署的應用程式來說,一個單獨的設定檔會更合適。
設定檔幾乎都是XML 檔案,XML 也的確很適合這一用途。不過,有些時候直接在程式碼中
實現裝配會更簡單。譬如一個簡單的應用程式,也沒有很多部署上的變化,這時用幾句代碼來配
置就比XML 檔案要清晰得多。
在Java 世界裡,我們聽到了來自設定檔的不和諧音——每個組件都有它自己的設定檔,而
且格式還各各不同。如果你要使用一打這樣的組件,你就得維護一打的設定檔,那會很快讓你
煩死。
在這裡,我的建議是:始終提供一種標準的配置方式,使程式員能夠通過同一個編程介面輕鬆地
完成配置工作。至於其他的設定檔,僅僅把它們當作一種可選的功能。藉助這個編程介面,開
發者可以輕鬆地管理設定檔。如果你編寫了一個組件,則可以由組件的使用者來選擇如何管理
配置資訊:使用你的編程介面、直接操作設定檔格式,或者定義他們自己的設定檔格式,並
將其與你的編程介面相結合。