深度理解依賴注入(Dependence Injection)

來源:互聯網
上載者:User

標籤:style   blog   http   color   io   os   使用   ar   strong   

前面的話:提到依賴注入,大家都會想到老馬那篇經典的文章。其實,本文就是相當於對那篇文章的解讀。所以,如果您對原文已經有了非常深刻的理解,完全不需要再看此文;但是,如果您和筆者一樣,以前曾經看過,似乎看懂了,但似乎又沒抓到什麼要領,不妨看看筆者這個解讀,也許對您理解原文有一定協助。

1.依賴在哪裡
   老馬舉了一個小例子,是開發一個電影列舉器(MovieList),這個電影列舉器需要使用一個電影尋找器(MovieFinder)提供的服務,偽碼如下:

 1/*服務的介面*/
 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中更常使用設值注入的方式:

 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.txt</value>
10        </property>
11    </bean>
12</beans>

下面也給出支援設值注入的容器參考實現,大家可以和構造器注入的容器對照起來看,裡面的差別很小,主要的差別就在於,在擷取對象執行個體(GetInstance)的時候,前者是通過反射得到待建立類型的構造器資訊,然後根據構造器傳入參數的類型在容器中進行尋找,並構造出合適的執行個體;而後者是通過反射得到待建立類型的所有屬性,然後根據屬性的類型在容器中尋找相應類型的執行個體。

設值注入的容器實現偽碼

2.3 Interface Injection (介面注入)
   這是筆者認為最不夠優雅的一種依賴注入方式。要實現介面注入,首先ServiceProvider要給出一個介面定義:

1public interface InjectFinder {
2    void injectFinder(MovieFinder finder);
3}

接下來,ServiceUser必須實現這個介面:

1class MovieLister: InjectFinder
2{
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執行個體:

1//服務定位器的初始化
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應用的優勢所在。

參考資料:
Object Builder Application Block

深度理解依賴注入(Dependence Injection)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.