1.依賴(lài)在哪里
老馬舉了一個(gè)小例子,是開(kāi)發(fā)一個(gè)電影列舉器(MovieList),這個(gè)電影列舉器需要使用一個(gè)電影查找器(MovieFinder)提供的服務(wù),偽碼如下:
1
/*服務(wù)的接口*/
2
public interface MovieFinder {
3
ArrayList findAll();
4
}
5
6
/*服務(wù)的消費者*/
7
class MovieLister
8
{
9
public Movie[] moviesDirectedBy(String arg) {
10
List allMovies = finder.findAll();
11
for (Iterator it = allMovies.iterator(); it.hasNext();) {
12
Movie movie = (Movie) it.next();
13
if (!movie.getDirector().equals(arg)) it.remove();
14
}
15
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
16
}
17
18
/*消費者內部包含一個(gè)將指向具體服務(wù)類(lèi)型的實(shí)體對象*/
19
private MovieFinder finder;
20
/*消費者需要在某一個(gè)時(shí)刻去實(shí)例化具體的服務(wù)。這是我們要解耦的關(guān)鍵所在,
21
*因為這樣的處理方式造成了服務(wù)消費者和服務(wù)提供者的強耦合關(guān)系(這種耦合是在編譯期就確定下來(lái)的)。
22
**/
23
public MovieLister() {
24
finder = new ColonDelimitedMovieFinder("movies1.txt");
25
}
26
}
從上面代碼的注釋中可以看到,MovieLister和ColonDelimitedMovieFinder(這可以使任意一個(gè)實(shí)現了MovieFinder接口的類(lèi)型)之間存在強耦合關(guān)系,如下圖所示:
圖1
這使得MovieList很難作為一個(gè)成熟的組件去發(fā)布,因為在不同的應用環(huán)境中(包括同一套軟件系統被不同用戶(hù)使用的時(shí)候),它所要依賴(lài)的電影查找器可能是千差萬(wàn)別的。所以,為了能實(shí)現真正的基于組件的開(kāi)發(fā),必須有一種機制能同時(shí)滿(mǎn)足下面兩個(gè)要求:
(1)解除MovieList對具體MoveFinder類(lèi)型的強依賴(lài)(編譯期依賴(lài))。
(2)在運行的時(shí)候為MovieList提供正確的MovieFinder類(lèi)型的實(shí)例。
換句話(huà)說(shuō),就是在運行的時(shí)候才產(chǎn)生MovieList和MovieFinder之間的依賴(lài)關(guān)系(把這種依賴(lài)關(guān)系在一個(gè)合適的時(shí)候“注入”運行時(shí)),這恐怕就是Dependency Injection這個(gè)術(shù)語(yǔ)的由來(lái)。再換句話(huà)說(shuō),我們提到過(guò)解除強依賴(lài),這并不是說(shuō)MovieList和MovieFinder之間的依賴(lài)關(guān)系不存在了,事實(shí)上MovieList無(wú)論如何也需要某類(lèi)MovieFinder提供的服務(wù),我們只是把這種依賴(lài)的建立時(shí)間推后了,從編譯器推遲到運行時(shí)了。
依賴(lài)關(guān)系在OO程序中是廣泛存在的,只要A類(lèi)型中用到了B類(lèi)型實(shí)例,A就依賴(lài)于B。前面筆者談到的內容是把概念抽象到了服務(wù)使用者和服務(wù)提供者的角度,這也符合現在SOA的設計思路。從另一種抽象方式上來(lái)看,可以把MovieList看成我們要構建的主系統,而MovieFinder是系統中的plugin,主系統并不強依賴(lài)于任何一個(gè)插件,但一旦插件被加載,主系統就應該可以準確調用適當插件的功能。
其實(shí)不管是面向服務(wù)的編程模式,還是基于插件的框架式編程,為了實(shí)現松耦合(服務(wù)調用者和提供者之間的or框架和插件之間的),都需要在必要的位置實(shí)現面向接口編程,在此基礎之上,還應該有一種方便的機制實(shí)現具體類(lèi)型之間的運行時(shí)綁定,這就是DI所要解決的問(wèn)題。
2.DI的實(shí)現方式
和上面的圖1對應的是,如果我們的系統實(shí)現了依賴(lài)注入,組件間的依賴(lài)關(guān)系就變成了圖2:
圖2
說(shuō)白了,就是要提供一個(gè)容器,由容器來(lái)完成(1)具體ServiceProvider的創(chuàng )建(2)ServiceUser和ServiceProvider的運行時(shí)綁定。下面我們就依次來(lái)看一下三種典型的依賴(lài)注入方式的實(shí)現。特別要說(shuō)明的是,要理解依賴(lài)注入的機制,關(guān)鍵是理解容器的實(shí)現方式。本文后面給出的容器參考實(shí)現,均為黃忠成老師的代碼,筆者僅在其中加上了一些關(guān)鍵注釋而已。
2.1 Constructor Injection(構造器注入)
我們可以看到,在整個(gè)依賴(lài)注入的數據結構中,涉及到的重要的類(lèi)型就是ServiceUser, ServiceProvider和Assembler三者,而這里所說(shuō)的構造器,指的是ServiceUser的構造器。也就是說(shuō),在構造ServiceUser實(shí)例的時(shí)候,才把真正的ServiceProvider傳給他:
1
class MovieLister
2
{
3
//其他內容,省略
4
5
public MovieLister(MovieFinder finder)
6
{
7
this.finder = finder;
8
}
9
}
接下來(lái)我們看看Assembler應該如何構建:
1
private MutablePicoContainer configureContainer() {
2
MutablePicoContainer pico = new DefaultPicoContainer();
3
4
//下面就是把ServiceProvider和ServiceUser都放入容器的過(guò)程,以后就由容器來(lái)提供ServiceUser的已完成依賴(lài)注入實(shí)例,
5
//其中用到的實(shí)例參數和類(lèi)型參數一般是從配置檔中讀取的,這里是個(gè)簡(jiǎn)單的寫(xiě)法。
6
//所有的依賴(lài)注入方法都會(huì )有類(lèi)似的容器初始化過(guò)程,本文在后面的小節中就不再重復這一段代碼了。
7
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
8
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
9
pico.registerComponentImplementation(MovieLister.class);
10
//至此,容器里面裝入了兩個(gè)類(lèi)型,其中沒(méi)給出構造參數的那一個(gè)(MovieLister)將依靠其在構造器中定義的傳入參數類(lèi)型,在容器中
11
//進(jìn)行查找,找到一個(gè)類(lèi)型匹配項即可進(jìn)行構造初始化。
12
return pico;
13
}
需要在強調一下的是,依賴(lài)并未消失,只是延后到了容器被構建的時(shí)刻。所以正如圖2中您已經(jīng)看到的,容器本身(更準確的說(shuō),是一個(gè)容器運行實(shí)例的構建過(guò)程)對ServiceUser和ServiceProvoder都是存在依賴(lài)關(guān)系的。所以,在這樣的體系結構里,ServiceUser、ServiceProvider和容器都是穩定的,互相之間也沒(méi)有任何依賴(lài)關(guān)系;所有的依賴(lài)關(guān)系、所有的變化都被封裝進(jìn)了容器實(shí)例的創(chuàng )建過(guò)程里,符合我們對服務(wù)應用的理解。而且,在實(shí)際開(kāi)發(fā)中我們一般會(huì )采用配置文件來(lái)輔助容器實(shí)例的創(chuàng )建,將這種變化性排斥到編譯期之外。
即使還沒(méi)給出后面的代碼,你也一定猜得到,這個(gè)container類(lèi)一定有一個(gè)GetInstance(Type t)這樣的方法,這個(gè)方法會(huì )為我們返回一個(gè)已經(jīng)注入完畢的MovieLister。 一個(gè)簡(jiǎn)單的應用如下:
1
public void testWithPico()
2
{
3
MutablePicoContainer pico = configureContainer();
4
MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
5
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
6
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
7
}
上面最關(guān)鍵的就是對pico.getComponentInstance的調用。Assembler會(huì )在這個(gè)時(shí)候調用MovieLister的構造器,構造器的參數就是當時(shí)通過(guò)pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams)設置進(jìn)去的實(shí)際的ServiceProvider--ColonMovieFinder。下面請看這個(gè)容器的參考代碼:
2.2 Setter Injection(設值注入)
這種注入方式和構造注入實(shí)在很類(lèi)似,唯一的區別就是前者在構造函數的調用過(guò)程中進(jìn)行注入,而它是通過(guò)給屬性賦值來(lái)進(jìn)行注入。無(wú)怪乎PicoContainer和Spring都是同時(shí)支持這兩種注入方式。Spring對通過(guò)XML進(jìn)行配置有比較好的支持,也使得Spring中更常使用設值注入的方式:
1
<beans>
2
<bean id="MovieLister" class="spring.MovieLister">
3
<property name="finder">
4
<ref local="MovieFinder"/>
5
property>
6
bean>
7
<bean id="MovieFinder" class="spring.ColonMovieFinder">
8
<property name="filename">
9
<value>movies1.txtvalue>
10
property>
11
bean>
12
beans>
下面也給出支持設值注入的容器參考實(shí)現,大家可以和構造器注入的容器對照起來(lái)看,里面的差別很小,主要的差別就在于,在獲取對象實(shí)例(GetInstance)的時(shí)候,前者是通過(guò)反射得到待創(chuàng )建類(lèi)型的構造器信息,然后根據構造器傳入參數的類(lèi)型在容器中進(jìn)行查找,并構造出合適的實(shí)例;而后者是通過(guò)反射得到待創(chuàng )建類(lèi)型的所有屬性,然后根據屬性的類(lèi)型在容器中查找相應類(lèi)型的實(shí)例。
2.3 Interface Injection (接口注入)
這是筆者認為最不夠優(yōu)雅的一種依賴(lài)注入方式。要實(shí)現接口注入,首先ServiceProvider要給出一個(gè)接口定義:
1
public interface InjectFinder {
2
void injectFinder(MovieFinder finder);
3
}
接下來(lái),ServiceUser必須實(shí)現這個(gè)接口:
1
class MovieLister: InjectFinder
2
{
3
public void injectFinder(MovieFinder finder) {
4
this.finder = finder;
5
}
6
}
容器所要做的,就是根據接口定義調用其中的inject方法完成注入過(guò)程,這里就不在贅述了,總的原理和上面兩種依賴(lài)注入模式?jīng)]有太多區別。
2.4 除了DI,還有Service Locator
上面提到的依賴(lài)注入只是消除ServiceUser和ServiceProvider之間的依賴(lài)關(guān)系的一種方法,還有另一種方法:服務(wù)定位器(Service Locator)。也就是說(shuō),由ServiceLocator來(lái)專(zhuān)門(mén)負責提供具體的ServiceProvider。當然,這樣的話(huà)ServiceUser不僅要依賴(lài)于服務(wù)的接口,還依賴(lài)于ServiceContract。仍然是最早提到過(guò)的電影列舉器的例子,如果使用Service Locator來(lái)解除依賴(lài)的話(huà),整個(gè)依賴(lài)關(guān)系應當如下圖所示:
圖3
用起來(lái)也很簡(jiǎn)單,在一個(gè)適當的位置(比如在一組相關(guān)服務(wù)即將被調用之前)對ServiceLocator進(jìn)行初始化,用到的時(shí)候就直接用ServiceLocator返回ServiceProvider實(shí)例:
1
//服務(wù)定位器的初始化
2
ServiceLocator locator = new ServiceLocator();
3
locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
4
ServiceLocator.load(locator);
5
//服務(wù)定義器的使用
6
//其實(shí)這個(gè)使用方式體現了服務(wù)定位器和依賴(lài)注入模式的最大差別:ServiceUser需要顯示的調用ServiceLocator,從而獲取自己需要的服務(wù)對象;
7
//而依賴(lài)注入則是隱式的由容器完成了這一切。
8
MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
9
正因為上面提到過(guò)的ServiceUser對ServiceLocator的依賴(lài)性,從提高模塊的獨立性(比如說(shuō),你可能把你構造的ServiceUser或者ServiceProvider給第三方使用)上來(lái)說(shuō),依賴(lài)注入可能更好一些,這恐怕也是為什么大多數的IOC框架都選用了DI的原因。ServiceLocator最大的優(yōu)點(diǎn)可能在于實(shí)現起來(lái)非常簡(jiǎn)單,如果您開(kāi)發(fā)的應用沒(méi)有復雜到需要采用一個(gè)IOC框架的程度,也許您可以試著(zhù)采用它。
3.廣義的服務(wù)
文中很多地方提到服務(wù)使用者(ServiceUser)和服務(wù)提供者(ServiceProvider)的概念,這里的“服務(wù)”是一種非常廣義的概念,在語(yǔ)法層面就是指最普通的依賴(lài)關(guān)系(類(lèi)型A中有一個(gè)B類(lèi)型的變量,則A依賴(lài)于B)。如果您把服務(wù)理解為WCF或者Web Service中的那種服務(wù)概念,您會(huì )發(fā)現上面所說(shuō)的所有技術(shù)手段都是沒(méi)有意義的。以WCF而論,其客戶(hù)端和服務(wù)器端本就是依賴(lài)于Contract的松耦合關(guān)系,其實(shí)這也從另一個(gè)角度說(shuō)明了SOA應用的優(yōu)勢所在。
參考資料:
Object Builder Application Block