DI 依赖注入实现原理
深度理解依賴注入(Dependence Injection)
前面的話:提到依賴注入,大家都會想到老馬那篇經典的文章。其實,本文就是相當于對那篇文章的解讀。所以,如果您對原文已經有了非常深刻的理解,完全不需要再看此文;但是,如果您和筆者一樣,以前曾經看過,似乎看懂了,但似乎又沒抓到什么要領,不妨看看筆者這個解讀,也許對您理解原文有一定幫助。
1.依賴在哪里
?? 老馬舉了一個小例子,是開發一個電影列舉器(MovieList),這個電影列舉器需要使用一個電影查找器(MovieFinder)提供的服務,偽碼如下:
?2public?interface?MovieFinder?{
?3????ArrayList?findAll();
?4}
?5
?6/*服務的消費者*/
?7class?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????/*消費者內部包含一個將指向具體服務類型的實體對象*/
19????private?MovieFinder?finder;
20????/*消費者需要在某一個時刻去實例化具體的服務。這是我們要解耦的關鍵所在,
21?????*因為這樣的處理方式造成了服務消費者和服務提供者的強耦合關系(這種耦合是在編譯期就確定下來的)。
22?????**/
23????public?MovieLister()?{
24????????finder?=?new?ColonDelimitedMovieFinder("movies1.txt");
25????}
26}
從上面代碼的注釋中可以看到,MovieLister和ColonDelimitedMovieFinder(這可以使任意一個實現了MovieFinder接口的類型)之間存在強耦合關系,如下圖所示:
圖1
這使得MovieList很難作為一個成熟的組件去發布,因為在不同的應用環境中(包括同一套軟件系統被不同用戶使用的時候),它所要依賴的電影查找器可能是千差萬別的。所以,為了能實現真正的基于組件的開發,必須有一種機制能同時滿足下面兩個要求:
?(1)解除MovieList對具體MoveFinder類型的強依賴(編譯期依賴)。
?(2)在運行的時候為MovieList提供正確的MovieFinder類型的實例。
???換句話說,就是在運行的時候才產生MovieList和MovieFinder之間的依賴關系(把這種依賴關系在一個合適的時候“注入”運行時),這恐怕就是Dependency Injection這個術語的由來。再換句話說,我們提到過解除強依賴,這并不是說MovieList和MovieFinder之間的依賴關系不存在了,事實上MovieList無論如何也需要某類MovieFinder提供的服務,我們只是把這種依賴的建立時間推后了,從編譯器推遲到運行時了。
?? 依賴關系在OO程序中是廣泛存在的,只要A類型中用到了B類型實例,A就依賴于B。前面筆者談到的內容是把概念抽象到了服務使用者和服務提供者的角度,這也符合現在SOA的設計思路。從另一種抽象方式上來看,可以把MovieList看成我們要構建的主系統,而MovieFinder是系統中的plugin,主系統并不強依賴于任何一個插件,但一旦插件被加載,主系統就應該可以準確調用適當插件的功能。
?? 其實不管是面向服務的編程模式,還是基于插件的框架式編程,為了實現松耦合(服務調用者和提供者之間的or框架和插件之間的),都需要在必要的位置實現面向接口編程,在此基礎之上,還應該有一種方便的機制實現具體類型之間的運行時綁定,這就是DI所要解決的問題。
2.DI的實現方式
?? 和上面的圖1對應的是,如果我們的系統實現了依賴注入,組件間的依賴關系就變成了圖2:
圖2
說白了,就是要提供一個容器,由容器來完成(1)具體ServiceProvider的創建(2)ServiceUser和ServiceProvider的運行時綁定。下面我們就依次來看一下三種典型的依賴注入方式的實現。特別要說明的是,要理解依賴注入的機制,關鍵是理解容器的實現方式。本文后面給出的容器參考實現,均為黃忠成老師的代碼,筆者僅在其中加上了一些關鍵注釋而已。
2.1 Constructor Injection(構造器注入)
?我們可以看到,在整個依賴注入的數據結構中,涉及到的重要的類型就是ServiceUser, ServiceProvider和Assembler三者,而這里所說的構造器,指的是ServiceUser的構造器。也就是說,在構造ServiceUser實例的時候,才把真正的ServiceProvider傳給他:
1class?MovieLister
2{
3???//其他內容,省略
4
5???public?MovieLister(MovieFinder?finder)
6???{
7???????this.finder?=?finder;
8???}
9} 接下來我們看看Assembler應該如何構建:
?1private?MutablePicoContainer?configureContainer()?{
?2????MutablePicoContainer?pico?=?new?DefaultPicoContainer();
?3????
?4????//下面就是把ServiceProvider和ServiceUser都放入容器的過程,以后就由容器來提供ServiceUser的已完成依賴注入實例,
?5????//其中用到的實例參數和類型參數一般是從配置檔中讀取的,這里是個簡單的寫法。
?6????//所有的依賴注入方法都會有類似的容器初始化過程,本文在后面的小節中就不再重復這一段代碼了。
?7????Parameter[]?finderParams?=??{new?ConstantParameter("movies1.txt")};
?8????pico.registerComponentImplementation(MovieFinder.class,?ColonMovieFinder.class,?finderParams);
?9????pico.registerComponentImplementation(MovieLister.class);
10????//至此,容器里面裝入了兩個類型,其中沒給出構造參數的那一個(MovieLister)將依靠其在構造器中定義的傳入參數類型,在容器中
11????//進行查找,找到一個類型匹配項即可進行構造初始化。
12????return?pico;
13} 需要在強調一下的是,依賴并未消失,只是延后到了容器被構建的時刻。所以正如圖2中您已經看到的,容器本身(更準確的說,是一個容器運行實例的構建過程)對ServiceUser和ServiceProvoder都是存在依賴關系的。所以,在這樣的體系結構里,ServiceUser、ServiceProvider和容器都是穩定的,互相之間也沒有任何依賴關系;所有的依賴關系、所有的變化都被封裝進了容器實例的創建過程里,符合我們對服務應用的理解。而且,在實際開發中我們一般會采用配置文件來輔助容器實例的創建,將這種變化性排斥到編譯期之外。
?? 即使還沒給出后面的代碼,你也一定猜得到,這個container類一定有一個GetInstance(Type t)這樣的方法,這個方法會為我們返回一個已經注入完畢的MovieLister。 一個簡單的應用如下:
1public?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} 上面最關鍵的就是對pico.getComponentInstance的調用。Assembler會在這個時候調用MovieLister的構造器,構造器的參數就是當時通過pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams)設置進去的實際的ServiceProvider--ColonMovieFinder。下面請看這個容器的參考代碼:
構造注入所需容器的偽碼
2.2 Setter Injection(設值注入)
?? 這種注入方式和構造注入實在很類似,唯一的區別就是前者在構造函數的調用過程中進行注入,而它是通過給屬性賦值來進行注入。無怪乎PicoContainer和Spring都是同時支持這兩種注入方式。Spring對通過XML進行配置有比較好的支持,也使得Spring中更常使用設值注入的方式:
?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.txt</value>
10????????</property>
11????</bean>
12</beans>
下面也給出支持設值注入的容器參考實現,大家可以和構造器注入的容器對照起來看,里面的差別很小,主要的差別就在于,在獲取對象實例(GetInstance)的時候,前者是通過反射得到待創建類型的構造器信息,然后根據構造器傳入參數的類型在容器中進行查找,并構造出合適的實例;而后者是通過反射得到待創建類型的所有屬性,然后根據屬性的類型在容器中查找相應類型的實例。
設值注入的容器實現偽碼2.3 Interface Injection (接口注入)
?? 這是筆者認為最不夠優雅的一種依賴注入方式。要實現接口注入,首先ServiceProvider要給出一個接口定義:
2????void?injectFinder(MovieFinder?finder);
3}
接下來,ServiceUser必須實現這個接口:
1class?MovieLister:?InjectFinder2{
3???public?void?injectFinder(MovieFinder?finder)?{
4??????this.finder?=?finder;
5????}
6}
容器所要做的,就是根據接口定義調用其中的inject方法完成注入過程,這里就不在贅述了,總的原理和上面兩種依賴注入模式沒有太多區別。
2.4? 除了DI,還有Service Locator
?? 上面提到的依賴注入只是消除ServiceUser和ServiceProvider之間的依賴關系的一種方法,還有另一種方法:服務定位器(Service Locator)。也就是說,由ServiceLocator來專門負責提供具體的ServiceProvider。當然,這樣的話ServiceUser不僅要依賴于服務的接口,還依賴于ServiceContract。仍然是最早提到過的電影列舉器的例子,如果使用Service Locator來解除依賴的話,整個依賴關系應當如下圖所示:
圖3
用起來也很簡單,在一個適當的位置(比如在一組相關服務即將被調用之前)對ServiceLocator進行初始化,用到的時候就直接用ServiceLocator返回ServiceProvider實例:
2ServiceLocator?locator?=?new?ServiceLocator();
3locator.loadService("MovieFinder",?new?ColonMovieFinder("movies1.txt"));
4ServiceLocator.load(locator);
5//服務定義器的使用
6//其實這個使用方式體現了服務定位器和依賴注入模式的最大差別:ServiceUser需要顯示的調用ServiceLocator,從而獲取自己需要的服務對象;
7//而依賴注入則是隱式的由容器完成了這一切。
8MovieFinder?finder?=?(MovieFinder)?ServiceLocator.getService("MovieFinder");
9
正因為上面提到過的ServiceUser對ServiceLocator的依賴性,從提高模塊的獨立性(比如說,你可能把你構造的ServiceUser或者ServiceProvider給第三方使用)上來說,依賴注入可能更好一些,這恐怕也是為什么大多數的IOC框架都選用了DI的原因。ServiceLocator最大的優點可能在于實現起來非常簡單,如果您開發的應用沒有復雜到需要采用一個IOC框架的程度,也許您可以試著采用它。
3.廣義的服務
?? 文中很多地方提到服務使用者(ServiceUser)和服務提供者(ServiceProvider)的概念,這里的“服務”是一種非常廣義的概念,在語法層面就是指最普通的依賴關系(類型A中有一個B類型的變量,則A依賴于B)。如果您把服務理解為WCF或者Web Service中的那種服務概念,您會發現上面所說的所有技術手段都是沒有意義的。以WCF而論,其客戶端和服務器端本就是依賴于Contract的松耦合關系,其實這也從另一個角度說明了SOA應用的優勢所在。
總結
以上是生活随笔為你收集整理的DI 依赖注入实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《梦仙》第三十一句是什么
- 下一篇: 电影表达的主题