控制反轉,依賴註入
最近在學習Spring框架,它的核心就是IoC容器。要掌握Spring框架,就必須要理解控制反轉的思想以及依賴註入的實現方式。那麽出現了以下問題
- 什麽是控制反轉?
- 什麽是依賴註入?
- 它們之間有什麽關系?
- 如何在Spring框架中應用依賴註入?
什麽是控制反轉
在討論控制反轉之前,我們先來看看軟件系統中耦合的對象。
為了解決對象間耦合度過高的問題,軟件專家Michael Mattson提出了IOC理論,用來實現對象之間的“解耦”。
控制反轉(Inversion of Control)是一種是面向對象編程中的一種設計原則,用來減低計算機代碼之間的耦合度。其基本思想是:借助於“第三方”實現具有依賴關系的對象之間的解耦。
- 軟件系統在沒有引入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只管使用就可以。
控制反轉,依賴註入