亚洲免费在线-亚洲免费在线播放-亚洲免费在线观看-亚洲免费在线观看视频-亚洲免费在线看-亚洲免费在线视频

IoC 容器和Dependency Injection 模式

系統 1594 0
撰文/Martin Fowler 編譯/透明
Java 社群近來掀起了一陣輕量級容器的熱潮,這些容器能夠幫助開發者將來自不同項目的組件
組裝成為一個內聚的應用程序。在它們的背后有著同一個模式,這個模式決定了這些容器進行組
件裝配的方式。人們用一個大而化之的名字來稱呼這個模式:“控制反轉”( Inversion of
Control IoC )。在本文中,我將深入探索這個模式的工作原理,給它一個更能描述其特點的名
字——“依賴注入”( Dependency Injection ),并將其與“服務定位器”( Service Locator
模式作一個比較。不過,這兩者之間的差異并不太重要,更重要的是:應該將組件的配置與使用
分離開——兩個模式的目標都是這個。
在企業級Java 的世界里存在一個有趣的現象:有很多人投入很多精力來研究主流J2EE 技術的
替代品——自然,這大多發生在open source 社群。在很大程度上,這可以看作是開發者對主
流J2EE 技術的笨重和復雜作出的回應,但其中的確有很多極富創意的想法,的確提供了一些可
供選擇的方案。J2EE 開發者常遇到的一個問題就是如何組裝不同的程序元素:如果web 控制
器體系結構和數據庫接口是由不同的團隊所開發的,彼此幾乎一無所知,你應該如何讓它們配合
工作?很多框架嘗試過解決這個問題,有幾個框架索性朝這個方向發展,提供了更通用的“組裝
各層組件”的方案。這樣的框架通常被稱為“輕量級容器”,PicoContainer 和Spring 都在此
列中。
在這些容器背后,一些有趣的設計原則發揮著作用。這些原則已經超越了特定容器的范疇,甚至
已經超越了Java 平臺的范疇。在本文中,我就要初步揭示這些原則。我使用的范例是Java 代
碼,但正如我的大多數文章一樣,這些原則也同樣適用于別的OO 環境,特別是.NET。
組件和服務
“裝配程序元素”,這樣的話題立即將我拖進了一個棘手的術語問題:如何區分“服務”(service)
和“組件”(component)?你可以毫不費力地找出關于這兩個詞定義的長篇大論,各種彼此
矛盾的定義會讓你感受到我所處的窘境。有鑒于此,對于這兩個遭到了嚴重濫用的詞匯,我將首
先說明它們在本文中的用法。
所謂“組件”是指這樣一個軟件單元:它將被作者無法控制的其他應用程序使用,但后者不能對
組件進行修改。也就是說,使用一個組件的應用程序不能修改組件的源代碼,但可以通過作者預
留的某種途徑對其進行擴展,以改變組件的行為。
服務和組件有某種相似之處:它們都將被外部的應用程序使用。在我看來,兩者之間最大的差異
在于:組件是在本地使用的(例如JAR 文件、程序集、DLL、或者源碼導入);而服務是要通過
——同步或異步的——遠程接口來遠程使用的(例如web service、消息系統、RPC,或者
socket)。
在本文中,我將主要使用“服務”這個詞,但文中的大多數邏輯也同樣適用于本地組件。實際上,
為了方便地訪問遠程服務,你往往需要某種本地組件框架。不過,“組件或者服務”這樣一個詞
組實在太麻煩了,而且“服務”這個詞當下也很流行,所以本文將用“服務”指代這兩者。
一個簡單的例子
為了更好地說明問題,我要引入一個例子。和我以前用的所有例子一樣,這是一個超級簡單的例
子:它非常小,小得有點不夠真實,但足以幫助你看清其中的道理,而不至于陷入真實例子的泥
潭中無法自拔。
在這個例子中,我編寫了一個組件,用于提供一份電影清單,清單上列出的影片都是由一位特定
的導演執導的。實現這個偉大的功能只需要一個方法:
class MovieLister...
??? public Movie[] moviesDirectedBy(String arg) {
??????? List allMovies = finder.findAll();
??????? for (Iterator it = allMovies.iterator(); it.hasNext();) {
??????????? Movie movie = (Movie) it.next();
??????????? if (!movie.getDirector().equals(arg)) it.remove();
??????? }
??????? return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
??? }
你可以看到,這個功能的實現極其簡單:moviesDirectedBy 方法首先請求finder(影片搜尋
者)對象(我們稍后會談到這個對象)返回后者所知道的所有影片,然后遍歷finder 對象返回
的清單,并返回其中由特定的某個導演執導的影片。非常簡單,不過不必擔心,這只是整個例子
的腳手架罷了。
我們真正想要考察的是finder對象,或者說,如何將MovieLister對象與特定的finder對象連
接起來。為什么我們對這個問題特別感興趣?因為我希望上面這個漂亮的moviesDirectedBy
方法完全不依賴于影片的實際存儲方式。所以,這個方法只能引用一個finder對象,而finder
對象則必須知道如何對findAll 方法作出回應。為了幫助讀者更清楚地理解,我給finder定義了
一個接口:
public interface MovieFinder {
??? List findAll();
}
現在,兩個對象之間沒有什么耦合關系。但是,當我要實際尋找影片時,就必須涉及到
MovieFinder 的某個具體子類。在這里,我把“涉及具體子類”的代碼放在MovieLister 類的
構造子中。
class MovieLister...
? private MovieFinder finder;
? public MovieLister() {
??? finder = new ColonDelimitedMovieFinder("movies1.txt");
? }
這個實現類的名字就說明:我將要從一個逗號分隔的文本文件中獲得影片列表。你不必操心具體
的實現細節,只要設想這樣一個實現類就可以了。
如果這個類只由我自己使用,一切都沒問題。但是,如果我的朋友嘆服于這個精彩的功能,也想
使用我的程序,那又會怎么樣呢?如果他們也把影片清單保存在一個逗號分隔的文本文件中,并
且也把這個文件命名為“ movie1.txt ”,那么一切還是沒問題。如果他們只是給這個文件改改名,
我也可以從一個配置文件獲得文件名,這也很容易。但是,如果他們用完全不同的方式——例如
SQL 數據庫、XML 文件、web service,或者另一種格式的文本文件——來存儲影片清單呢?
在這種情況下,我們需要用另一個類來獲取數據。由于已經定義了MovieFinder接口,我可以
不用修改moviesDirectedBy 方法。但是,我仍然需要通過某種途徑獲得合適的MovieFinder
實現類的實例。
IoC 容器和Dependency Injection 模式 ?
圖1:“在MovieLister 類中直接創建MovieFinder 實例”時的依賴關系
?
圖1 展現了這種情況下的依賴關系:MovieLister 類既依賴于MovieFinder接口,也依賴于具
體的實現類。我們當然希望MovieLister 類只依賴于接口,但我們要如何獲得一個MovieFinder
子類的實例呢?
Patterns of Enterprise Application Architecture 一書中,我們把這種情況稱為“插件”
(plugin):MovieFinder的實現類不是在編譯期連入程序之中的,因為我并不知道我的朋友會
使用哪個實現類。我們希望MovieLister 類能夠與MovieFinder的任何實現類配合工作,并且
允許在運行期插入具體的實現類,插入動作完全脫離我(原作者)的控制。這里的問題就是:如
何設計這個連接過程,使MovieLister 類在不知道實現類細節的前提下與其實例協同工作。
將這個例子推而廣之,在一個真實的系統中,我們可能有數十個服務和組件。在任何時候,我們
總可以對使用組件的情形加以抽象,通過接口與具體的組件交流(如果組件并沒有設計一個接口,
也可以通過適配器與之交流)。但是,如果我們希望以不同的方式部署這個系統,就需要用插件
機制來處理服務之間的交互過程,這樣我們才可能在不同的部署方案中使用不同的實現。
所以,現在的核心問題就是:如何將這些插件組合成一個應用程序?這正是新生的輕量級容器所
面臨的主要問題,而它們解決這個問題的手段無一例外地是控制反轉(Inversion of Control)
模式。
控制反轉
幾位輕量級容器的作者曾驕傲地對我說:這些容器非常有用,因為它們實現了“控制反轉”。這
樣的說辭讓我深感迷惑:控制反轉是框架所共有的特征,如果僅僅因為使用了控制反轉就認為這
些輕量級容器與眾不同,就好象在說“我的轎車是與眾不同的,因為它有四個輪子”。
問題的關鍵在于:它們反轉了哪方面的控制?我第一次接觸到的控制反轉針對的是用戶界面的主
控權。早期的用戶界面是完全由應用程序來控制的,你預先設計一系列命令,例如“輸入姓名”、
“輸入地址”等,應用程序逐條輸出提示信息,并取回用戶的響應。而在圖形用戶界面環境下,
UI 框架將負責執行一個主循環,你的應用程序只需為屏幕的各個區域提供事件處理函數即可。
在這里,程序的主控權發生了反轉:從應用程序移到了框架。
對于這些新生的容器,它們反轉的是“如何定位插件的具體實現”。在前面那個簡單的例子中,
MovieLister 類負責定位MovieFinder 的具體實現——它直接實例化后者的一個子類。這樣一
來,MovieFinder 也就不成其為一個插件了,因為它并不是在運行期插入應用程序中的。而這
些輕量級容器則使用了更為靈活的辦法,只要插件遵循一定的規則,一個獨立的組裝模塊就能夠
將插件的具體實現“注射”到應用程序中。
因此,我想我們需要給這個模式起一個更能說明其特點的名字——“控制反轉”這個名字太泛了,
常常讓人有些迷惑。與多位IoC 愛好者討論之后,我們決定將這個模式叫做“依賴注入”
(Dependency Injection)。
下面,我將開始介紹Dependency Injection 模式的幾種不同形式。不過,在此之前,我要首
先指出:要消除應用程序對插件實現的依賴,依賴注入并不是唯一的選擇,你也可以用Service
Locator 模式獲得同樣的效果。介紹完Dependency Injection 模式之后,我也會談到Service
Locator 模式。
依賴注入的幾種形式
Dependency Injection 模式的基本思想是:用一個單獨的對象(裝配器)來獲得MovieFinder
的一個合適的實現,并將其實例賦給MovieLister 類的一個字段。這樣一來,我們就得到了圖2所示的依賴圖: IoC 容器和Dependency Injection 模式
圖2:引入依賴注入器之后的依賴關系
依賴注入的形式主要有三種,我分別將它們叫做構造子注入(Constructor Injection)、設值
方法注入(Setter Injection)和接口注入(Interface Injection)。如果讀過最近關于IoC 的
一些討論材料,你不難看出:這三種注入形式分別就是type 1 IoC(接口注入)、type 2 IoC
(設值方法注入)和type 3 IoC(構造子注入)。我發現數字編號往往比較難記,所以我使用了
這里的命名方式。
使用PicoContainer 進行構造子注入
首先,我要向讀者展示如何用一個名為PicoContainer 的輕量級容器完成依賴注入。之所以從
這里開始,主要是因為我在ThoughtWorks 公司的幾個同事在PicoContainer 的開發社群中
非常活躍——沒錯,也可以說是某種偏袒吧。
PicoContainer 通過構造子來判斷“如何將MovieFinder 實例注入MovieLister 類”。因此,
MovieLister 類必須聲明一個構造子,并在其中包含所有需要注入的元素:
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
MovieFinder 實例本身也將由PicoContainer來管理,因此文本文件的名字也可以由容器注入:
class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
隨后,需要告訴PicoContainer:各個接口分別與哪個實現類關聯、將哪個字符串注入
MovieFinder組件。
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = {new
ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class,
ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}
這段配置代碼通常位于另一個類。對于我們這個例子,使用我的MovieLister 類的朋友需要在
自己的設置類中編寫合適的配置代碼。當然,還可以將這些配置信息放在一個單獨的配置文件中,
這也是一種常見的做法。你可以編寫一個類來讀取配置文件,然后對容器進行合適的設置。盡管
PicoContainer 本身并不包含這項功能,但另一個與它關系緊密的項目NanoContainer 提供
了一些包裝,允許開發者使用XML 配置文件保存配置信息。NanoContainer能夠解析XML 文
件,并對底下的PicoContainer 進行配置。這個項目的哲學觀念就是:將配置文件的格式與底
下的配置機制分離開。
使用這個容器,你寫出的代碼大概會是這樣:
public void testWithPico() {
MutablePicoContainer pico = configureContainer();
MovieLister lister = (MovieLister)
pico.getComponentInstance(MovieLister.class);
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West",
movies[0].getTitle());
}
盡管在這里我使用了構造子注入,實際上PicoContainer 也支持設值方法注入,不過該項目的
開發者更推薦使用構造子注入。
使用Spring 進行設值方法注入
Spring 框架是一個用途廣泛的企業級Java 開發框架,其中包括了針對事務、持久化框架、web
應用開發和JDBC 等常用功能的抽象。和PicoContainer 一樣,它也同時支持構造子注入和設
值方法注入,但該項目的開發者更推薦使用設值方法注入——恰好適合這個例子。
為了讓MovieLister 類接受注入, 我需要為它定義一個設值方法,該方法接受類型為
MovieFinder的參數:
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
類似地,在MovieFinder的實現類中,我也定義了一個設值方法,接受類型為String 的參數:
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
第三步是設定配置文件。Spring 支持多種配置方式,你可以通過XML 文件進行配置,也可以
直接在代碼中配置。不過,XML 文件是比較理想的配置方式。
<beans>
<bean id="MovieLister" class="spring.MovieLister">
<property name="finder">
<ref local="MovieFinder"/>
</property>
</bean>
<bean id="MovieFinder" class="spring.ColonMovieFinder">
<property name="filename">
<value>movies1.txt</value>
</property>
</bean>
</beans>
于是,測試代碼大概就像下面這樣:
public void testWithSpring() throws Exception {
ApplicationContext ctx = new
FileSystemXmlApplicationContext("spring.xml");
MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West",
movies[0].getTitle());
}
接口注入
除了前面兩種注入技術,還可以在接口中定義需要注入的信息,并通過接口完成注入。Avalon
框架就使用了類似的技術。在這里,我首先用簡單的范例代碼說明它的用法,后面還會有更深入
的討論。
首先,我需要定義一個接口,組件的注入將通過這個接口進行。在本例中,這個接口的用途是將
一個MovieFinder實例注入繼承了該接口的對象。
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
這個接口應該由提供MovieFinder 接口的人一并提供。任何想要使用MovieFinder 實例的類
(例如MovieLister 類)都必須實現這個接口。
class MovieLister implements InjectFinder...
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
然后,我使用類似的方法將文件名注入MovieFinder的實現類:
public interface InjectFilename {
void injectFilename (String filename);
}
class ColonMovieFinder implements MovieFinder, InjectFilename......
public void injectFilename(String filename) {
this.filename = filename;
}
現在,還需要用一些配置代碼將所有的組件實現裝配起來。簡單起見,我直接在代碼中完成配置,
并將配置好的MovieLister 對象保存在名為lister的字段中:
class IfaceTester...
private MovieLister lister;
private void configureLister() {
ColonMovieFinder finder = new ColonMovieFinder();
finder.injectFilename("movies1.txt");
lister = new MovieLister();
lister.injectFinder(finder);
}
測試代碼則可以直接使用這個字段:
class IfaceTester...
public void testIface() {
configureLister();
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West",
movies[0].getTitle());
}
使用 Service Locator
依賴注入的最大好處在于:它消除了MovieLister類對具體MovieFinder實現類的依賴。這樣
一來, 我就可以把MovieLister 類交給朋友, 讓他們根據自己的環境插入一個合適的
MovieFinder實現即可。不過,Dependency Injection 模式并不是打破這層依賴關系的唯一
手段,另一種方法是使用Service Locator 模式。
Service Locator 模式背后的基本思想是:有一個對象(即服務定位器)知道如何獲得一個應用
程序所需的所有服務。也就是說,在我們的例子中,服務定位器應該有一個方法,用于獲得一個
MovieFinder 實例。當然,這不過是把麻煩換了一個樣子,我們仍然必須在MovieLister 中獲
得服務定位器,最終得到的依賴關系如圖3 所示:
IoC 容器和Dependency Injection 模式
圖3:使用Service Locator 模式之后的依賴關系
?
在這里,我把ServiceLocator 類實現為一個Singleton 的注冊表,于是MovieLister 就可以
在實例化時通過ServiceLocator 獲得一個MovieFinder 實例。
class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
public static MovieFinder movieFinder() {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
和注入的方式一樣,我們也必須對服務定位器加以配置。在這里,我直接在代碼中進行配置,但
設計一種通過配置文件獲得數據的機制也并非難事。
class Tester...
private void configure() {
ServiceLocator.load(new ServiceLocator(new
ColonMovieFinder("movies1.txt")));
}
class ServiceLocator...
public static void load(ServiceLocator arg) {
soleInstance = arg;
}
public ServiceLocator(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
下面是測試代碼:
class Tester...
public void testSimple() {
configure();
MovieLister lister = new MovieLister();
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West",
movies[0].getTitle());
}
我時常聽到這樣的論調:這樣的服務定位器不是什么好東西,因為你無法替換它返回的服務實現,
從而導致無法對它們進行測試。當然,如果你的設計很糟糕,你的確會遇到這樣的麻煩;但你也
可以選擇良好的設計。在這個例子中,ServiceLocator 實例僅僅是一個簡單的數據容器,只需
要對它做一些簡單的修改,就可以讓它返回用于測試的服務實現。
對于更復雜的情況,我可以從ServiceLocator 派生出多個子類,并將子類型的實例傳遞給注冊
表的類變量。另外,我可以修改ServiceLocator 的靜態方法,使其調用ServiceLocator 實例
的方法,而不是直接訪問實例變量。我還可以使用特定于線程的存儲機制,從而提供特定于線程
的服務定位器。所有這一切改進都無須修改ServiceLocator 的使用者。
一種改進的思路是:服務定位器仍然是一個注冊表,但不是Singleton。Singleton 的確是實現
注冊表的一種簡單途徑,但這只是一個實現時的決定,可以很輕松地改變它。
為定位器提供分離的接口
上面這種簡單的實現方式有一個問題:MovieLister 類將依賴于整個ServiceLocator 類,但它
需要使用的卻只是后者所提供的一項服務。我們可以針對這項服務提供一個單獨的接口,減少
MovieLister 對ServiceLocator 的依賴程度。這樣一來,MovieLister 就不必使用整個的
ServiceLocator 接口,只需聲明它想要使用的那部分接口。
此時,MovieLister 類的提供者也應該一并提供一個定位器接口,使用者可以通過這個接口獲得
MovieFinder實例。
public interface MovieFinderLocator {
public MovieFinder movieFinder();
真實的服務定位器需要實現上述接口,提供訪問MovieFinder 實例的能力:
MovieFinderLocator locator = ServiceLocator.locator();
MovieFinder finder = locator.movieFinder();
public static ServiceLocator locator() {
return soleInstance;
}
public MovieFinder movieFinder() {
return movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
你應該已經注意到了:由于想要使用接口,我們不能再通過靜態方法直接訪問服務——我們必須
首先通過ServiceLocator 類獲得定位器實例,然后使用定位器實例得到我們想要的服務。
動態服務定位器
上面是一個靜態定位器的例子——對于你所需要的每項服務,ServiceLocator 類都有對應的方
法。這并不是實現服務定位器的唯一方式,你也可以創建一個動態服務定位器,你可以在其中注
冊需要的任何服務,并在運行期決定獲得哪一項服務。
在本例中,ServiceLocator 使用一個map 來保存服務信息,而不再是將這些信息保存在字段
中。此外,ServiceLocator 還提供了一個通用的方法,用于獲取和加載服務對象。
class ServiceLocator...
private static ServiceLocator soleInstance;
public static void load(ServiceLocator arg) {
soleInstance = arg;
}
private Map services = new HashMap();
public static Object getService(String key){
return soleInstance.services.get(key);
}
public void loadService (String key, Object service) {
services.put(key, service);
}
同樣需要對服務定位器進行配置,將服務對象與適當的關鍵字加載到定位器中:
class Tester...
private void configure() {
ServiceLocator locator = new ServiceLocator();
locator.loadService("MovieFinder", new
ColonMovieFinder("movies1.txt"));
ServiceLocator.load(locator);
}
我使用與服務對象類名稱相同的字符串作為服務對象的關鍵字:
class MovieLister...
MovieFinder finder = (MovieFinder)
ServiceLocator.getService("MovieFinder");
總體而言,我不喜歡這種方式。無疑,這樣實現的服務定位器具有更強的靈活性,但它的使用方
式不夠直觀明朗。我只有通過文本形式的關鍵字才能找到一個服務對象。相比之下,我更欣賞“通
過一個方法明確獲得服務對象”的方式,因為這讓使用者能夠從接口定義中清楚地知道如何獲得
某項服務。
用Avalon 兼顧服務定位器和依賴注入
Dependency Injection 和Service Locator 兩個模式并不是互斥的,你可以同時使用它們,
Avalon 框架就是這樣的一個例子。Avalon 使用了服務定位器,但“如何獲得定位器”的信息
則是通過注入的方式告知組件的。
對于前面一直使用的例子,Berin Loritsch 發送給了我一個簡單的Avalon 實現版本:
public class MyMovieLister implements MovieLister, Serviceable {
private MovieFinder finder;
public void service( ServiceManager manager )
throws ServiceException
{
finder = (MovieFinder)manager.lookup("finder");
}
service 方法就是接口注入的例子, 它使容器可以將一個ServiceManager 對象注入
MyMovieLister 對象。ServiceManager則是一個服務定位器。在這個例子中,MyMovieLister
并不把ServiceManager 對象保存在字段中,而是馬上借助它找到MovieFinder 實例,并將
后者保存起來。
作出一個選擇
到現在為止,我一直在闡述自己對這兩個模式( Dependency Injection 模式和Service
Locator 模式)以及它們的變化形式的看法。現在,我要開始討論他們的優點和缺點,以便指出
它們各自適用的場景。
Service Locator vs. Dependency Injection
首先,我們面臨Service Locator 和Dependency Injection 之間的選擇。應該注意,盡管我
們前面那個簡單的例子不足以表現出來,實際上這兩個模式都提供了基本的解耦合能力——無論
使用哪個模式,應用程序代碼都不依賴于服務接口的具體實現。兩者之間最重要的區別在于:這
個“具體實現”以什么方式提供給應用程序代碼。使用Service Locator 模式時,應用程序代
碼直接向服務定位器發送一個消息,明確要求服務的實現;使用Dependency Injection 模式
時,應用程序代碼不發出顯式的請求,服務的實現自然會出現在應用程序代碼中,這也就是所謂
“控制反轉”
控制反轉是框架的共同特征,但它也要求你付出一定的代價:它會增加理解的難度,并且給調試
帶來一定的困難。所以,整體來說,除非必要,否則我會盡量避免使用它。這并不意味著控制反
轉不好,只是我認為在很多時候使用一個更為直觀的方案(例如Service Locator 模式)會比
較合適。
一個關鍵的區別在于:使用Service Locator 模式時,服務的使用者必須依賴于服務定位器。
定位器可以隱藏使用者對服務具體實現的依賴,但你必須首先看到定位器本身。所以,問題的答
案就很明朗了:選擇Service Locator 還是Dependency Injection,取決于“對定位器的依
賴”是否會給你帶來麻煩。
Dependency Injection 模式可以幫助你看清組件之間的依賴關系:你只需觀察依賴注入的機
制(例如構造子),就可以掌握整個依賴關系。而使用Service Locator 模式時,你就必須在源
代碼中到處搜索對服務定位器的調用。具備全文檢索能力的IDE 可以略微簡化這一工作,但還
是不如直接觀察構造子或者設值方法來得輕松。
這個選擇主要取決于服務使用者的性質。如果你的應用程序中有很多不同的類要使用一個服務,
那么應用程序代碼對服務定位器的依賴就不是什么大問題。在前面的例子中,我要把
MovieLister 類交給朋友去用,這種情況下使用服務定位器就很好:我的朋友們只需要對定位器
做一點配置(通過配置文件或者某些配置性的代碼),使其提供合適的服務實現就可以了。在這
種情況下,我看不出Dependency Injection 模式提供的控制反轉有什么吸引人的地方。
但是,如果把MovieLister 看作一個組件,要將它提供給別人寫的應用程序去使用,情況就不
同了。在這種時候,我無法預測使用者會使用什么樣的服務定位器API,每個使用者都可能有自
己的服務定位器,而且彼此之間無法兼容。一種解決辦法是為每項服務提供單獨的接口,使用者
可以編寫一個適配器,讓我的接口與他們的服務定位器相配合。但即便如此,我仍然需要到第一
個服務定位器中尋找我規定的接口。而且一旦用上了適配器,服務定位器所提供的簡單性就被大
大削弱了。
另一方面,如果使用Dependency Injection 模式,組件與注入器之間不會有依賴關系,因此
組件無法從注入器那里獲得更多的服務,只能獲得配置信息中所提供的那些。這也是
Dependency Injection 模式的局限性之一。
人們傾向于使用Dependency Injection 模式的一個常見理由是:它簡化了測試工作。這里的
關鍵是:出于測試的需要,你必須能夠輕松地在“真實的服務實現”與“供測試用的‘偽’組件”
之間切換。但是,如果單從這個角度來考慮,Dependency Injection 模式和Service Locator
模式其實并沒有太大區別:兩者都能夠很好地支持“偽”組件的插入。之所以很多人有
“Dependency Injection 模式更利于測試”的印象,我猜是因為他們并沒有努力保證服務定
位器的可替換性。這正是持續測試起作用的地方:如果你不能輕松地用一些“偽”組件將一個服
務架起來以便測試,這就意味著你的設計出現了嚴重的問題。
當然,如果組件環境具有非常強的侵略性(就像EJB 框架那樣),測試的問題會更加嚴重。我的
觀點是:應該盡量減少這類框架對應用程序代碼的影響,特別是不要做任何可能使“編輯-執行”
的循環變慢的事情。用插件(plugin)機制取代重量級組件會對測試過程有很大幫助,這正是
測試驅動開發(Test Driven Development,TDD)之類實踐的關鍵所在。
所以,主要的問題在于:代碼的作者是否希望自己編寫的組件能夠脫離自己的控制、被使用在另
一個應用程序中。如果答案是肯定的,那么他就不能對服務定位器做任何假設——哪怕最小的假
設也會給使用者帶來麻煩。
構造子注入 vs. 設值方法注入
在組合服務時,你總得遵循一定的約定,才可能將所有東西拼裝起來。依賴注入的優點主要在于:
它只需要非常簡單的約定——至少對于構造子注入和設值方法注入來說是這樣。相比于這兩者,
接口注入的侵略性要強得多,比起Service Locator 模式的優勢也不那么明顯。
所以,如果你想要提供一個組件給多個使用者,構造子注入和設值方法注入看起來很有吸引力:
你不必在組件中加入什么希奇古怪的東西,注入器可以相當輕松地把所有東西配置起來。
設值函數注入和構造子注入之間的選擇相當有趣,因為它折射出面向對象編程的一些更普遍的問
題:應該在哪里填充對象的字段,構造子還是設值方法?
一直以來,我首選的做法是盡量在構造階段就創建完整、合法的對象——也就是說,在構造子中
填充對象字段。這樣做的好處可以追溯到Kent Beck 在 Smalltalk Best Practice Patterns
一書中介紹的兩個模式:Constructor Method 和Constructor Parameter Method。帶有參
數的構造子可以明確地告訴你如何創建一個合法的對象。如果創建合法對象的方式不止一種,你
還可以提供多個構造子,以說明不同的組合方式。
構造子初始化的另一個好處是:你可以隱藏任何不可變的字段——只要不為它提供設值方法就行
了。我認為這很重要:如果某個字段是不應該被改變的,“沒有針對該字段的設值方法”就很清
楚地說明了這一點。如果你通過設值方法完成初始化,暴露出來的設值方法很可能成為你心頭永
遠的痛。(實際上,在這種時候我更愿意回避通常的設值方法約定,而是使用諸如initFoo 之類
的方法名,以表明該方法只應該在對象創建之初調用。)
不過,世事總有例外。如果參數太多,構造子會顯得凌亂不堪,特別是對于不支持關鍵字參數的
語言更是如此。的確,如果構造子參數列表太長,通常標志著對象太過繁忙,理應將其拆分成幾
個對象,但有些時候也確實需要那么多的參數。
如果有不止一種的方式可以構造一個合法的對象,也很難通過構造子描述這一信息,因為構造子
之間只能通過參數的個數和類型加以區分。這就是Factory Method 模式適用的場合了,工廠
方法可以借助多個私有構造子和設值方法的組合來完成自己的任務。經典Factory Method 模
式的問題在于:它們往往以靜態方法的形式出現,你無法在接口中聲明它們。你可以創建一個工
廠類,但那又變成另一個服務實體了。“工廠服務”是一種不錯的技巧,但你仍然需要以某種方
式實例化這個工廠對象,問題仍然沒有解決。
如果要傳入的參數是像字符串這樣的簡單類型,構造子注入也會帶來一些麻煩。使用設值方法注
入時,你可以在每個設值方法的名字中說明參數的用途;而使用構造子注入時,你只能靠參數的
位置來決定每個參數的作用,而記住參數的正確位置顯然要困難得多。
如果對象有多個構造子,對象之間又存在繼承關系,事情就會變得特別討厭。為了讓所有東西都
正確地初始化,你必須將對子類構造子的調用轉發給超類的構造子,然后處理自己的參數。這可
能造成構造子規模的進一步膨脹。
盡管有這些缺陷,但我仍然建議你首先考慮構造子注入。不過,一旦前面提到的問題真的成了問
題,你就應該準備轉為使用設值方法注入。
在將Dependecy Injection 模式作為框架的核心部分的幾支團隊之間,“構造子注入還是設值
方法注入”引發了很多的爭論。不過,現在看來,開發這些框架的大多數人都已經意識到:不管
更喜歡哪種注入機制,同時為兩者提供支持都是有必要的。
代碼配置 vs. 配置文件
另一個問題相對獨立,但也經常與其他問題牽涉在一起:如何配置服務的組裝,通過配置文件還
是直接編碼組裝?對于大多數需要在多處部署的應用程序來說,一個單獨的配置文件會更合適。
配置文件幾乎都是XML 文件,XML 也的確很適合這一用途。不過,有些時候直接在程序代碼中
實現裝配會更簡單。譬如一個簡單的應用程序,也沒有很多部署上的變化,這時用幾句代碼來配
置就比XML 文件要清晰得多。
與之相對的,有時應用程序的組裝非常復雜,涉及大量的條件步驟。一旦編程語言中的配置邏輯
開始變得復雜,你就應該用一種合適的語言來描述配置信息,使程序邏輯變得更清晰。然后,你
可以編寫一個構造器(builder)類來完成裝配工作。如果使用構造器的情景不止一種,你可以
提供多個構造器類,然后通過一個簡單的配置文件在它們之間選擇。
我常常發現,人們太急于定義配置文件。編程語言通常會提供簡捷而強大的配置管理機制,現代
編程語言也可以將程序編譯成小的模塊,并將其插入大型系統中。如果編譯過程會很費力,腳本
語言也可以在這方面提供幫助。
通常認為,配置文件不應該用編程語言來編寫,因為它們需要能夠被不懂編程的系統管理人員編
輯。但是,這種情況出現的幾率有多大呢?我們真的希望不懂編程的系統管理人員來改變一個復
雜的服務器端應用程序的事務隔離等級嗎?只有在非常簡單的時候,非編程語言的配置文件才有
最好的效果。如果配置信息開始變得復雜,就應該考慮選擇一種合適的編程語言來編寫配置文件。
在Java 世界里,我們聽到了來自配置文件的不和諧音——每個組件都有它自己的配置文件,而
且格式還各各不同。如果你要使用一打這樣的組件,你就得維護一打的配置文件,那會很快讓你
煩死。
在這里,我的建議是:始終提供一種標準的配置方式,使程序員能夠通過同一個編程接口輕松地
完成配置工作。至于其他的配置文件,僅僅把它們當作一種可選的功能。借助這個編程接口,開
發者可以輕松地管理配置文件。如果你編寫了一個組件,則可以由組件的使用者來選擇如何管理
配置信息:使用你的編程接口、直接操作配置文件格式,或者定義他們自己的配置文件格式,并
將其與你的編程接口相結合。
分離配置與使用
所有這一切的關鍵在于:服務的配置應該與使用分開。實際上,這是一個基本的設計原則——分
離接口與實現。在面向對象程序里,我們在一個地方用條件邏輯來決定具體實例化哪一個類,以
后的條件分支都由多態來實現,而不是繼續重復前面的條件邏輯,這就是“分離接口與實現”的
原則。
如果對于一段代碼而言,接口與實現的分離還只是“有用”的話,那么當你需要使用外部元素(例
如組件和服務)時,它就是生死攸關的大事。這里的第一個問題是:你是否希望將“選擇具體實
現類”的決策推遲到部署階段。如果是,那么你需要使用插入技術。使用了插入技術之后,插件
的裝配原則上是與應用程序的其余部分分開的,這樣你就可以輕松地針對不同的部署替換不同的
配置。這種配置機制可以通過服務定位器來實現(Service Locator 模式),也可以借助依賴注
入直接完成(Dependency Injection 模式)。
更多的問題
在本文中,我關注的焦點是使用Dependency Injection 模式和Service Locator 模式進行服
務配置的基本問題。還有一些與之相關的話題值得關注,但我已經沒有時間繼續申發下去了。特
別值得注意的是生命周期行為的問題:某些組件具有特定的生命周期事件,例如“停止”、“開始”
等等。另一個值得注意的問題是:越來越多的人對“如何在這些容器中運用面向方面(aspect
oriented)的思想”產生了興趣。盡管目前還沒有認真準備過這方面的材料,但我也很希望以
后能在這個話題上寫一些東西。
關于這些問題,你在專注于輕量級容器的網站上可以找到很多資料。瀏覽PicoContainer
(http://www.picocontainer.org)或者Spring(http://www.springframework.org)的
網站,你可以找到大量相關的討論,并由此引申出更多的話題。
結論和思考
在時下流行的輕量級容器都使用了一個共同的模式來組裝應用程序所需的服務,我把這個模式稱
為Dependency Injection,它可以有效地替代Service Locator 模式。在開發應用程序時,
兩者不相上下,但我認為Service Locator 模式略有優勢,因為它的行為方式更為直觀。但是,
如果你開發的組件要交給多個應用程序去使用,那么Dependency Injection 模式會是更好的
選擇。
如果你決定使用Dependency Injection 模式,這里還有幾種不同的風格可供選擇。我建議你
首先考慮構造子注入;如果遇到了某些特定的問題,再改用設值方法注入。如果你要選擇一個容
器,在其之上進行開發,我建議你選擇同時支持這兩種注入方式的容器。
Service Locator 模式和Dependency Injection 模式之間的選擇并是最重要的,更重要的是:
應該將服務的配置和應用程序內部對服務的使用分離開。
致謝
在此,我要向幫助我理解本文中所提到的問題、并對本文提出寶貴意見的幾個人表示感謝,他們
是Rod Johnson、Paul Hammant、Joe Walnes、Aslak Hellesoy、Jon Tirsen 和Bill Caputo。
另外,Berin Loritsch 和Hamilton Verissimo de Oliveira 在Avalon 方面給了我非常有用的
建議,一并向他們表示感謝。
?

IoC 容器和Dependency Injection 模式


更多文章、技術交流、商務合作、聯系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 国产成人精品一区二三区2022 | 日本免费一区二区久久人人澡 | 日本a∨在线播放高清 | 够爱久久 | 天天草天天| 国产欧美日韩中文久久 | 国产乱码精品一区二区三区四川 | 日韩中文字幕高清在线专区 | 美女天天干 | 香蕉tv亚洲专区在线观看 | 欧洲成人在线观看 | 精品中文字幕一区在线 | 奇米影视亚洲春色 | 久久99亚洲精品久久99 | 国产日韩一区二区三区 | 国产亚洲精品一区二区在线播放 | 日韩欧美一二三区 | 99精品视频在线这里只有 | 片在线观看 | 狠狠综合视频精品播放 | 久久天天躁狠狠躁夜夜不卡 | 最新毛片久热97免费精品视频 | 不卡一级aaa全黄毛片 | 香蕉视频国产 | 日本视频三区 | 996热精品视频在线观看 | 国产97色在线 | 亚洲 | 精品国产九九 | 国产三级精品三级男人的天堂 | 亚洲欧美乱综合图片区小说区 | 久久精品爱国产免费久久 | 欧美午夜大片 | 色综合久久综合欧美综合网 | 黑人巨大精品战中国美女 | 你懂得在线网站 | 热灸灸这里只有精品 | 素人巨乳被调教 | 在线观看国产一区二三区 | 色综合天天干 | 国产精品欧美日韩视频一区 | 亚洲一区二区三区精品视频 |