1. 程式人生 > >控制反轉,依賴註入

控制反轉,依賴註入

per rec contex 單例 分銷 cfb 5% sse 合並

最近在學習Spring框架,它的核心就是IoC容器。要掌握Spring框架,就必須要理解控制反轉的思想以及依賴註入的實現方式。那麽出現了以下問題

  • 什麽是控制反轉?
  • 什麽是依賴註入?
  • 它們之間有什麽關系?
  • 如何在Spring框架中應用依賴註入?

什麽是控制反轉

在討論控制反轉之前,我們先來看看軟件系統中耦合的對象。
技術分享圖1:軟件系統中耦合的對象
從圖中可以看到,軟件中的對象就像齒輪一樣,協同工作,但是互相耦合,一個零件不能正常工作,整個系統就崩潰了。這是一個強耦合的系統。齒輪組中齒輪之間的嚙合關系,與軟件系統中對象之間的耦合關系非常相似。對象之間的耦合關系是無法避免的,也是必要的,這是協同工作的基礎。現在,伴隨著工業級應用的規模越來越龐大,對象之間的依賴關系也越來越復雜,經常會出現對象之間的多重依賴性關系,因此,架構師和設計師對於系統的分析和設計,將面臨更大的挑戰。對象之間耦合度過高的系統,必然會出現牽一發而動全身的情形。

為了解決對象間耦合度過高的問題,軟件專家Michael Mattson提出了IOC理論,用來實現對象之間的“解耦”。

控制反轉(Inversion of Control)是一種是面向對象編程中的一種設計原則,用來減低計算機代碼之間的耦合度。其基本思想是:借助於“第三方”實現具有依賴關系的對象之間的解耦。
技術分享圖2:IOC解耦過程
由於引進了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個對象沒有了耦合關系,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有對象粘合在一起發揮作用,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯系,這就是有人把IOC容器比喻成“粘合劑”的由來。
我們再來看看,控制反轉(IOC)到底為什麽要起這麽個名字?我們來對比一下:

  1. 軟件系統在沒有引入IOC容器之前,如圖1所示,對象A依賴於對象B,那麽對象A在初始化或者運行到某一點的時候,自己必須主動去創建對象B或者使用已經創建的對象B。無論是創建還是使用對象B,控制權都在自己手上。
  2. 軟件系統在引入IOC容器之後,這種情形就完全改變了,如圖2所示,由於IOC容器的加入,對象A與對象B之間失去了直接聯系,所以,當對象A運行到需要對象B的時候,IOC容器會主動創建一個對象B註入到對象A需要的地方。

通過前後的對比,我們不難看出來:對象A獲得依賴對象B的過程,由主動行為變為了被動行為,控制權顛倒過來了,這就是“控制反轉”這個名稱的由來。

控制反轉不只是軟件工程的理論,在生活中我們也有用到這種思想。再舉一個現實生活的例子:
海爾公司作為一個電器制商需要把自己的商品分銷到全國各地,但是發現,不同的分銷渠道有不同的玩法,於是派出了各種銷售代表玩不同的玩法,隨著渠道越來越多,發現,每增加一個渠道就要新增一批人和一個新的流程,嚴重耦合並依賴各渠道商的玩法。實在受不了了,於是制定業務標準,開發分銷信息化系統,只有符合這個標準的渠道商才能成為海爾的分銷商。讓各個渠道商反過來依賴自己標準。反轉了控制,倒置了依賴。

我們把海爾和分銷商當作軟件對象,分銷信息化系統當作IOC容器,可以發現,在沒有IOC容器之前,分銷商就像圖1中的齒輪一樣,增加一個齒輪就要增加多種依賴在其他齒輪上,勢必導致系統越來越復雜。開發分銷系統之後,所有分銷商只依賴分銷系統,就像圖2顯示那樣,可以很方便的增加和刪除齒輪上去。

什麽是依賴註入

依賴註入就是將實例變量傳入到一個對象中去(Dependency injection means giving an object its instance variables)。

什麽是依賴

如果在 Class A 中,有 Class B 的實例,則稱 Class A 對 Class B 有一個依賴。例如下面類 Human 中用到一個 Father 對象,我們就說類 Human 對類 Father 有一個依賴。

public class Human {
    ...
    Father father;
    ...
    public Human() {
        father = new Father();
    }
}

仔細看這段代碼我們會發現存在一些問題:

  1. 如果現在要改變 father 生成方式,如需要用new Father(String name)初始化 father,需要修改 Human 代碼;
  2. 如果想測試不同 Father 對象對 Human 的影響很困難,因為 father 的初始化被寫死在了 Human 的構造函數中;
  3. 如果new Father()過程非常緩慢,單測時我們希望用已經初始化好的 father 對象 Mock 掉這個過程也很困難。

依賴註入

上面將依賴在構造函數中直接初始化是一種 Hard init 方式,弊端在於兩個類不夠獨立,不方便測試。我們還有另外一種 Init 方式,如下:

public class Human {
    ...
    Father father;
    ...
    public Human(Father father) {
        this.father = father;
    }
}

上面代碼中,我們將 father 對象作為構造函數的一個參數傳入。在調用 Human 的構造方法之前外部就已經初始化好了 Father 對象。像這種非自己主動初始化依賴,而通過外部來傳入依賴的方式,我們就稱為依賴註入。
現在我們發現上面 1 中存在的兩個問題都很好解決了,簡單的說依賴註入主要有兩個好處:

  1. 解耦,將依賴之間解耦。
  2. 因為已經解耦,所以方便做單元測試,尤其是 Mock 測試。

控制反轉和依賴註入的關系

我們已經分別解釋了控制反轉和依賴註入的概念。有些人會把控制反轉和依賴註入等同,但實際上它們有著本質上的不同。

  • 控制反轉是一種思想
  • 依賴註入是一種設計模式

IoC框架使用依賴註入作為實現控制反轉的方式,但是控制反轉還有其他的實現方式,例如說ServiceLocator,所以不能將控制反轉和依賴註入等同。

Spring中的依賴註入

上面我們提到,依賴註入是實現控制反轉的一種方式。下面我們結合Spring的IoC容器,簡單描述一下這個過程。

class MovieLister...
    private MovieFinder finder;
    public void setFinder(MovieFinder finder) {
        this.finder = finder;
    }

class ColonMovieFinder...
    public void setFilename(String filename) {
        this.filename = filename;
    }

我們先定義兩個類,可以看到都使用了依賴註入的方式,通過外部傳入依賴,而不是自己創建依賴。那麽問題來了,誰把依賴傳給他們,也就是說誰負責創建finder,並且把finder傳給MovieLister。答案是Spring的IoC容器。

要使用IoC容器,首先要進行配置。這裏我們使用xml的配置,也可以通過代碼註解方式配置。下面是spring.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>

在Spring中,每個bean代表一個對象的實例,默認是單例模式,即在程序的生命周期內,所有的對象都只有一個實例,進行重復使用。通過配置bean,IoC容器在啟動的時候會根據配置生成bean實例。具體的配置語法參考Spring文檔。這裏只要知道IoC容器會根據配置創建MovieFinder,在運行的時候把MovieFinder賦值給MovieListerfinder屬性,完成依賴註入的過程。

下面給出測試代碼

public void testWithSpring() throws Exception {
    ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");//1
    MovieLister lister = (MovieLister) ctx.getBean("MovieLister");//2
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
  1. 根據配置生成ApplicationContext,即IoC容器。
  2. 從容器中獲取MovieLister的實例。

總結

  1. 控制反轉是一種在軟件工程中解耦合的思想,調用類只依賴接口,而不依賴具體的實現類,減少了耦合。控制權交給了容器,在運行的時候才由容器決定將具體的實現動態的“註入”到調用類的對象中。
  2. 依賴註入是一種設計模式,可以作為控制反轉的一種實現方式。依賴註入就是將實例變量傳入到一個對象中去(Dependency injection means giving an object its instance variables)。
  3. 通過IoC框架,類A依賴類B的強耦合關系可以在運行時通過容器建立,也就是說把創建B實例的工作移交給容器,類A只管使用就可以。

控制反轉,依賴註入