8.(轉)控制反轉(IoC)與依賴注入(DI)

團長李雲龍發表於2018-12-29

前言

最近在學習Spring框架,它的核心就是IoC容器。要掌握Spring框架,就必須要理解控制反轉的思想以及依賴注入的實現方式。下面,我們將圍繞下面幾個問題來探討控制反轉與依賴注入的關係以及在Spring中如何應用。

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

什麼是控制反轉

在討論控制反轉之前,我們先來看看軟體系統中耦合的物件。

軟體系統中耦合的物件

從圖中可以看到,軟體中的物件就像齒輪一樣,協同工作,但是互相耦合,一個零件不能正常工作,整個系統就崩潰了。這是一個強耦合的系統。齒輪組中齒輪之間的齧合關係,與軟體系統中物件之間的耦合關係非常相似。物件之間的耦合關係是無法避免的,也是必要的,這是協同工作的基礎。現在,伴隨著工業級應用的規模越來越龐大,物件之間的依賴關係也越來越複雜,經常會出現物件之間的多重依賴性關係,因此,架構師和設計師對於系統的分析和設計,將面臨更大的挑戰。物件之間耦合度過高的系統,必然會出現牽一髮而動全身的情形。 為了解決物件間耦合度過高的問題,軟體專家Michael Mattson提出了IOC理論,用來實現物件之間的“解耦”。 控制反轉(Inversion of Control)是一種是物件導向程式設計中的一種設計原則,用來減低計算機程式碼之間的耦合度。其基本思想是:藉助於“第三方”實現具有依賴關係的物件之間的解耦。

IOC解耦過程

由於引進了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個物件沒有了耦合關係,齒輪之間的傳動全部依靠“第三方”了,全部物件的控制權全部上繳給“第三方”IOC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有物件粘合在一起發揮作用,如果沒有這個“粘合劑”,物件與物件之間會彼此失去聯絡,這就是有人把IOC容器比喻成“粘合劑”的由來。 我們再來看看,控制反轉(IOC)到底為什麼要起這麼個名字?我們來對比一下:

軟體系統在沒有引入IOC容器之前,如圖1所示,物件A依賴於物件B,那麼物件A在初始化或者執行到某一點的時候,自己必須主動去建立物件B或者使用已經建立的物件B。無論是建立還是使用物件B,控制權都在自己手上。 軟體系統在引入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();
    }
}
複製程式碼

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

如果現在要改變 father 生成方式,如需要用new Father(String name)初始化 father,需要修改 Human 程式碼; 如果想測試不同 Father 物件對 Human 的影響很困難,因為 father 的初始化被寫死在了 Human 的建構函式中; 如果new Father()過程非常緩慢,單測時我們希望用已經初始化好的 father 物件 Mock 掉這個過程也很困難。 依賴注入 上面將依賴在建構函式中直接初始化是一種 Hard init 方式,弊端在於兩個類不夠獨立,不方便測試。我們還有另外一種 Init 方式,如下:

public class Human {
    ...
    Father father;
    ...
    public Human(Father father) {
        this.father = father;
    }
}
複製程式碼

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

解耦,將依賴之間解耦。 因為已經解耦,所以方便做單元測試,尤其是 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賦值給MovieLister的finder屬性,完成依賴注入的過程。

下面給出測試程式碼

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());
}
複製程式碼

根據配置生成ApplicationContext,即IoC容器。 從容器中獲取MovieLister的例項。

總結

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

WiHongNoteBook

相關文章