轉(zhuǎn)載鏈接:http://blog.xiaohansong.com/2015/10/21/IoC-and-DI/#
https://www.zhihu.com/question/23277575
前言
最近在學(xué)習(xí)Spring框架,它的核心就是IoC容器。要掌握Spring框架,就必須要理解控制反轉(zhuǎn)的思想以及依賴注入的實(shí)現(xiàn)方式。下面,我們將圍繞下面幾個問題來探討控制反轉(zhuǎn)與依賴注入的關(guān)系以及在Spring中如何應(yīng)用。
- 什么是控制反轉(zhuǎn)?
- 什么是依賴注入?
- 它們之間有什么關(guān)系?
- 如何在Spring框架中應(yīng)用依賴注入?
控制反轉(zhuǎn)
在討論控制反轉(zhuǎn)之前,我們先來看看軟件系統(tǒng)中耦合的對象。
從圖中可以看到,軟件中的對象就像齒輪一樣,協(xié)同工作,但是互相耦合,一個零件不能正常工作,整個系統(tǒng)就崩潰了。這是一個強(qiáng)耦合的系統(tǒng)。齒輪組中齒輪之間的嚙合關(guān)系,與軟件系統(tǒng)中對象之間的耦合關(guān)系非常相似。對象之間的耦合關(guān)系是無法避免的,也是必要的,這是協(xié)同工作的基礎(chǔ)。現(xiàn)在,伴隨著工業(yè)級應(yīng)用的規(guī)模越來越龐大,對象之間的依賴關(guān)系也越來越復(fù)雜,經(jīng)常會出現(xiàn)對象之間的多重依賴性關(guān)系,因此,架構(gòu)師和設(shè)計師對于系統(tǒng)的分析和設(shè)計,將面臨更大的挑戰(zhàn)。對象之間耦合度過高的系統(tǒng),必然會出現(xiàn)牽一發(fā)而動全身的情形。
為了解決對象間耦合度過高的問題,軟件專家Michael Mattson提出了IoC理論,用來實(shí)現(xiàn)對象之間的“解耦”。
控制反轉(zhuǎn)(Inversion of Control)是一種是面向?qū)ο缶幊讨械囊环N設(shè)計原則,用來減低計算機(jī)代碼之間的耦合度。其基本思想是:借助于“第三方”實(shí)現(xiàn)具有依賴關(guān)系的對象之間的解耦。
由于引進(jìn)了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個對象沒有了耦合關(guān)系,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權(quán)全部上繳給“第三方”IOC容器,所以,IOC容器成了整個系統(tǒng)的關(guān)鍵核心,它起到了一種類似“粘合劑”的作用,把系統(tǒng)中的所有對象粘合在一起發(fā)揮作用,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯(lián)系,這就是有人把IOC容器比喻成“粘合劑”的由來。
我們再來看看,控制反轉(zhuǎn)(IOC)到底為什么要起這么個名字?我們來對比一下:
軟件系統(tǒng)在沒有引入IOC容器之前,如圖1所示,對象A依賴于對象B,那么對象A在初始化或者運(yùn)行到某一點(diǎn)的時候,自己必須主動去創(chuàng)建對象B或者使用已經(jīng)創(chuàng)建的對象B。無論是創(chuàng)建還是使用對象B,控制權(quán)都在自己手上。
軟件系統(tǒng)在引入IOC容器之后,這種情形就完全改變了,如圖2所示,由于IOC容器的加入,對象A與對象B之間失去了直接聯(lián)系,所以,當(dāng)對象A運(yùn)行到需要對象B的時候,IOC容器會主動創(chuàng)建一個對象B注入到對象A需要的地方。
通過前后的對比,我們不難看出來:對象A獲得依賴對象B的過程,由主動行為變?yōu)榱吮粍有袨椋刂茩?quán)顛倒過來了,這就是“控制反轉(zhuǎn)”這個名稱的由來。
控制反轉(zhuǎn)不只是軟件工程的理論,在生活中我們也有用到這種思想。再舉一個現(xiàn)實(shí)生活的例子:
海爾公司作為一個電器制商需要把自己的商品分銷到全國各地,但是發(fā)現(xiàn),不同的分銷渠道有不同的玩法,于是派出了各種銷售代表玩不同的玩法,隨著渠道越來越多,發(fā)現(xiàn),每增加一個渠道就要新增一批人和一個新的流程,嚴(yán)重耦合并依賴各渠道商的玩法。實(shí)在受不了了,于是制定業(yè)務(wù)標(biāo)準(zhǔn),開發(fā)分銷信息化系統(tǒng),只有符合這個標(biāo)準(zhǔn)的渠道商才能成為海爾的分銷商。讓各個渠道商反過來依賴自己標(biāo)準(zhǔn)。反轉(zhuǎn)了控制,倒置了依賴。
我們把海爾和分銷商當(dāng)作軟件對象,分銷信息化系統(tǒng)當(dāng)作IOC容器,可以發(fā)現(xiàn),在沒有IOC容器之前,分銷商就像圖1中的齒輪一樣,增加一個齒輪就要增加多種依賴在其他齒輪上,勢必導(dǎo)致系統(tǒng)越來越復(fù)雜。開發(fā)分銷系統(tǒng)之后,所有分銷商只依賴分銷系統(tǒng),就像圖2顯示那樣,可以很方便的增加和刪除齒輪上去。
依賴注入
依賴注入就是將實(shí)例變量傳入到一個對象中去(Dependency injection means giving an object its instance variables)。
什么是依賴
如果在 Class A 中,有 Class B 的實(shí)例,則稱 Class A 對 Class B 有一個依賴。例如下面類 Human 中用到一個 Father 對象,我們就說類 Human 對類 Father 有一個依賴。
public class Human {
...
Father father;
...
public Human() {
father = new Father();
}
}
仔細(xì)看這段代碼我們會發(fā)現(xiàn)存在一些問題:
- 如果現(xiàn)在要改變 father 生成方式,如需要用new Father(String name)初始化 father,需要修改 Human 代碼;
- 如果想測試不同 Father 對象對 Human 的影響很困難,因?yàn)?father 的初始化被寫死在了 Human 的構(gòu)造函數(shù)中;
- 如果new Father()過程非常緩慢,單測時我們希望用已經(jīng)初始化好的 father 對象 Mock 掉這個過程也很困難。
依賴注入
上面將依賴在構(gòu)造函數(shù)中直接初始化是一種 Hard init 方式,弊端在于兩個類不夠獨(dú)立,不方便測試。我們還有另外一種 Init 方式,如下:
public class Human {
...
Father father;
...
public Human(Father father) {
this.father = father;
}
}
上面代碼中,我們將 father 對象作為構(gòu)造函數(shù)的一個參數(shù)傳入。在調(diào)用 Human 的構(gòu)造方法之前外部就已經(jīng)初始化好了 Father 對象。像這種非自己主動初始化依賴,而通過外部來傳入依賴的方式,我們就稱為依賴注入。
現(xiàn)在我們發(fā)現(xiàn)上面 1 中存在的兩個問題都很好解決了,簡單的說依賴注入主要有兩個好處:
- 解耦,將依賴之間解耦。
- 因?yàn)橐呀?jīng)解耦,所以方便做單元測試,尤其是 Mock 測試。
控制反轉(zhuǎn)和依賴注入的關(guān)系
我們已經(jīng)分別解釋了控制反轉(zhuǎn)和依賴注入的概念。有些人會把控制反轉(zhuǎn)和依賴注入等同,但實(shí)際上它們有著本質(zhì)上的不同。
- 控制反轉(zhuǎn)是一種思想
- 依賴注入是一種設(shè)計模式
IoC框架使用依賴注入作為實(shí)現(xiàn)控制反轉(zhuǎn)的方式,但是控制反轉(zhuǎn)還有其他的實(shí)現(xiàn)方式,例如說ServiceLocator,所以不能將控制反轉(zhuǎn)和依賴注入等同。
Spring中的依賴注入
上面我們提到,依賴注入是實(shí)現(xiàn)控制反轉(zhuǎn)的一種方式。下面我們結(jié)合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;
}
我們先定義兩個類,可以看到都使用了依賴注入的方式,通過外部傳入依賴,而不是自己創(chuàng)建依賴。那么問題來了,誰把依賴傳給他們,也就是說誰負(fù)責(zé)創(chuàng)建finder,并且把finder傳給MovieLister。答案是Spring的IoC容器。
要使用IoC容器,首先要進(jìn)行配置。這里我們使用xml的配置,也可以通過代碼注解方式配置。下面是spring.xml的內(nèi)容
<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代表一個對象的實(shí)例,默認(rèn)是單例模式,即在程序的生命周期內(nèi),所有的對象都只有一個實(shí)例,進(jìn)行重復(fù)使用。通過配置bean,IoC容器在啟動的時候會根據(jù)配置生成bean實(shí)例。具體的配置語法參考Spring文檔。這里只要知道IoC容器會根據(jù)配置創(chuàng)建MovieFinder,在運(yùn)行的時候把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());
}
1. 根據(jù)配置生成ApplicationContext,即IoC容器。
2. 從容器中獲取MovieLister的實(shí)例。
總結(jié)
1. 控制反轉(zhuǎn)是一種在軟件工程中解耦合的思想,調(diào)用類只依賴接口,而不依賴具體的實(shí)現(xiàn)類,減少了耦合。控制權(quán)交給了容器,在運(yùn)行的時候才由容器決定將具體的實(shí)現(xiàn)動態(tài)的“注入”到調(diào)用類的對象中。
2. 依賴注入是一種設(shè)計模式,可以作為控制反轉(zhuǎn)的一種實(shí)現(xiàn)方式。依賴注入就是將實(shí)例變量傳入到一個對象中去(Dependency injection means giving an object its instance variables)。
3. 通過IoC框架,類A依賴類B的強(qiáng)耦合關(guān)系可以在運(yùn)行時通過容器建立,也就是說把創(chuàng)建B實(shí)例的工作移交給容器,類A只管使用就可以。
下面的是參考自知乎。
要了解控制反轉(zhuǎn)( Inversion of Control ), 我覺得有必要先了解軟件設(shè)計的一個重要思想:依賴倒置原則(Dependency Inversion Principle )。
什么是依賴倒置原則?假設(shè)我們設(shè)計一輛汽車:先設(shè)計輪子,然后根據(jù)輪子大小設(shè)計底盤,接著根據(jù)底盤設(shè)計車身,最后根據(jù)車身設(shè)計好整個汽車。這里就出現(xiàn)了一個“依賴”關(guān)系:汽車依賴車身,車身依賴底盤,底盤依賴輪子。
這樣的設(shè)計看起來沒問題,但是可維護(hù)性卻很低。假設(shè)設(shè)計完工之后,上司卻突然說根據(jù)市場需求的變動,要我們把車子的輪子設(shè)計都改大一碼。這下我們就蛋疼了:因?yàn)槲覀兪歉鶕?jù)輪子的尺寸設(shè)計的底盤,輪子的尺寸一改,底盤的設(shè)計就得修改;同樣因?yàn)槲覀兪歉鶕?jù)底盤設(shè)計的車身,那么車身也得改,同理汽車設(shè)計也得改——整個設(shè)計幾乎都得改!
我們現(xiàn)在換一種思路。我們先設(shè)計汽車的大概樣子,然后根據(jù)汽車的樣子來設(shè)計車身,根據(jù)車身來設(shè)計底盤,最后根據(jù)底盤來設(shè)計輪子。這時候,依賴關(guān)系就倒置過來了:輪子依賴底盤, 底盤依賴車身, 車身依賴汽車。
這時候,上司再說要改動輪子的設(shè)計,我們就只需要改動輪子的設(shè)計,而不需要動底盤,車身,汽車的設(shè)計了。
這就是依賴倒置原則——把原本的高層建筑依賴底層建筑“倒置”過來,變成底層建筑依賴高層建筑。高層建筑決定需要什么,底層去實(shí)現(xiàn)這樣的需求,但是高層并不用管底層是怎么實(shí)現(xiàn)的。這樣就不會出現(xiàn)前面的“牽一發(fā)動全身”的情況。
**控制反轉(zhuǎn)(Inversion of Control) **就是依賴倒置原則的一種代碼設(shè)計的思路。具體采用的方法就是所謂的依賴注入(Dependency Injection)。其實(shí)這些概念初次接觸都會感到云里霧里的。說穿了,這幾種概念的關(guān)系大概如下:
為了理解這幾個概念,我們還是用上面汽車的例子。只不過這次換成代碼。我們先定義四個Class,車,車身,底盤,輪胎。然后初始化這輛車,最后跑這輛車。代碼結(jié)構(gòu)如下:
這樣,就相當(dāng)于上面第一個例子,上層建筑依賴下層建筑——每一個類的構(gòu)造函數(shù)都直接調(diào)用了底層代碼的構(gòu)造函數(shù)。假設(shè)我們需要改動一下輪胎(Tire)類,把它的尺寸變成動態(tài)的,而不是一直都是30。我們需要這樣改:
由于我們修改了輪胎的定義,為了讓整個程序正常運(yùn)行,我們需要做以下改動:
由此我們可以看到,僅僅是為了修改輪胎的構(gòu)造函數(shù),這種設(shè)計卻需要修改整個上層所有類的構(gòu)造函數(shù)!在軟件工程中,這樣的設(shè)計幾乎是不可維護(hù)的——在實(shí)際工程項(xiàng)目中,有的類可能會是幾千個類的底層,如果每次修改這個類,我們都要修改所有以它作為依賴的類,那軟件的維護(hù)成本就太高了。
所以我們需要進(jìn)行控制反轉(zhuǎn)(IoC),及上層控制下層,而不是下層控制著上層。我們用依賴注入(Dependency Injection)這種方式來實(shí)現(xiàn)控制反轉(zhuǎn)。所謂依賴注入,就是把底層類作為參數(shù)傳入上層類,實(shí)現(xiàn)上層類對下層類的“控制”。這里我們用構(gòu)造方法傳遞的依賴注入方式重新寫車類的定義:
這里我們再把輪胎尺寸變成動態(tài)的,同樣為了讓整個系統(tǒng)順利運(yùn)行,我們需要做如下修改:
看到?jīng)]?這里我只需要修改輪胎類就行了,不用修改其他任何上層類。這顯然是更容易維護(hù)的代碼。不僅如此,在實(shí)際的工程中,這種設(shè)計模式還有利于不同組的協(xié)同合作和單元測試:比如開發(fā)這四個類的分別是四個不同的組,那么只要定義好了接口,四個不同的組可以同時進(jìn)行開發(fā)而不相互受限制;而對于單元測試,如果我們要寫Car類的單元測試,就只需要Mock一下Framework類傳入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再來構(gòu)造Car。
這里我們是采用的構(gòu)造函數(shù)傳入的方式進(jìn)行的依賴注入。其實(shí)還有另外兩種方法:Setter傳遞和接口傳遞。這里就不多講了,核心思路都是一樣的,都是為了實(shí)現(xiàn)控制反轉(zhuǎn)。
看到這里你應(yīng)該能理解什么控制反轉(zhuǎn)和依賴注入了。那什么是控制反轉(zhuǎn)容器(IoC Container)呢?其實(shí)上面的例子中,對車類進(jìn)行初始化的那段代碼發(fā)生的地方,就是控制反轉(zhuǎn)容器。
顯然你也應(yīng)該觀察到了,因?yàn)椴捎昧艘蕾囎⑷耄诔跏蓟倪^程中就不可避免的會寫大量的new。這里IoC容器就解決了這個問題。這個容器可以自動對你的代碼進(jìn)行初始化,你只需要維護(hù)一個Configuration(可以是xml可以是一段代碼),而不用每次初始化一輛車都要親手去寫那一大段初始化的代碼。這是引入IoC Container的第一個好處。
IoC Container的第二個好處是:我們在創(chuàng)建實(shí)例的時候不需要了解其中的細(xì)節(jié)。在上面的例子中,我們自己手動創(chuàng)建一個車instance時候,是從底層往上層new的:
這個過程中,我們需要了解整個Car/Framework/Bottom/Tire類構(gòu)造函數(shù)是怎么定義的,才能一步一步new/注入。
而IoC Container在進(jìn)行這個工作的時候是反過來的,它先從最上層開始往下找依賴關(guān)系,到達(dá)最底層之后再往上一步一步new(有點(diǎn)像深度優(yōu)先遍歷):
這里IoC Container可以直接隱藏具體的創(chuàng)建實(shí)例的細(xì)節(jié),在我們來看它就像一個工廠:
我們就像是工廠的客戶。我們只需要向工廠請求一個Car實(shí)例,然后它就給我們按照Config創(chuàng)建了一個Car實(shí)例。我們完全不用管這個Car實(shí)例是怎么一步一步被創(chuàng)建出來。
實(shí)際項(xiàng)目中,有的Service Class可能是十年前寫的,有幾百個類作為它的底層。假設(shè)我們新寫的一個API需要實(shí)例化這個Service,我們總不可能回頭去搞清楚這幾百個類的構(gòu)造函數(shù)吧?IoC Container的這個特性就很完美的解決了這類問題——因?yàn)檫@個架構(gòu)要求你在寫class的時候需要寫相應(yīng)的Config文件,所以你要初始化很久以前的Service類的時候,前人都已經(jīng)寫好了Config文件,你直接在需要用的地方注入這個Service就可以了。這大大增加了項(xiàng)目的可維護(hù)性且降低了開發(fā)難度。