轉載鏈接:http://blog.xiaohansong.com/2015/10/21/IoC-and-DI/#
https://www.zhihu.com/question/23277575
前言
最近在學習Spring框架玫氢,它的核心就是IoC容器她肯。要掌握Spring框架萧恕,就必須要理解控制反轉的思想以及依賴注入的實現(xiàn)方式戒良。下面歹苦,我們將圍繞下面幾個問題來探討控制反轉與依賴注入的關系以及在Spring中如何應用。
- 什么是控制反轉醋闭?
- 什么是依賴注入镰绎?
- 它們之間有什么關系?
- 如何在Spring框架中應用依賴注入耍群?
控制反轉
在討論控制反轉之前义桂,我們先來看看軟件系統(tǒng)中耦合的對象。
從圖中可以看到蹈垢,軟件中的對象就像齒輪一樣慷吊,協(xié)同工作,但是互相耦合曹抬,一個零件不能正常工作溉瓶,整個系統(tǒng)就崩潰了。這是一個強耦合的系統(tǒng)谤民。齒輪組中齒輪之間的嚙合關系,與軟件系統(tǒng)中對象之間的耦合關系非常相似堰酿。對象之間的耦合關系是無法避免的,也是必要的张足,這是協(xié)同工作的基礎〈ゴ矗現(xiàn)在,伴隨著工業(yè)級應用的規(guī)模越來越龐大为牍,對象之間的依賴關系也越來越復雜嗅榕,經(jīng)常會出現(xiàn)對象之間的多重依賴性關系,因此吵聪,架構師和設計師對于系統(tǒng)的分析和設計,將面臨更大的挑戰(zhàn)兼雄。對象之間耦合度過高的系統(tǒng)吟逝,必然會出現(xiàn)牽一發(fā)而動全身的情形。
為了解決對象間耦合度過高的問題赦肋,軟件專家Michael Mattson提出了IoC理論块攒,用來實現(xiàn)對象之間的“解耦”励稳。
控制反轉(Inversion of Control)是一種是面向對象編程中的一種設計原則,用來減低計算機代碼之間的耦合度囱井。其基本思想是:借助于“第三方”實現(xiàn)具有依賴關系的對象之間的解耦驹尼。
由于引進了中間位置的“第三方”,也就是IOC容器庞呕,使得A新翎、B、C住练、D這4個對象沒有了耦合關系地啰,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC容器讲逛,所以亏吝,IOC容器成了整個系統(tǒng)的關鍵核心,它起到了一種類似“粘合劑”的作用盏混,把系統(tǒng)中的所有對象粘合在一起發(fā)揮作用蔚鸥,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯(lián)系许赃,這就是有人把IOC容器比喻成“粘合劑”的由來止喷。
我們再來看看,控制反轉(IOC)到底為什么要起這么個名字图焰?我們來對比一下:
軟件系統(tǒng)在沒有引入IOC容器之前启盛,如圖1所示,對象A依賴于對象B技羔,那么對象A在初始化或者運行到某一點的時候僵闯,自己必須主動去創(chuàng)建對象B或者使用已經(jīng)創(chuàng)建的對象B。無論是創(chuàng)建還是使用對象B藤滥,控制權都在自己手上鳖粟。
軟件系統(tǒng)在引入IOC容器之后,這種情形就完全改變了拙绊,如圖2所示向图,由于IOC容器的加入,對象A與對象B之間失去了直接聯(lián)系标沪,所以榄攀,當對象A運行到需要對象B的時候,IOC容器會主動創(chuàng)建一個對象B注入到對象A需要的地方金句。
通過前后的對比檩赢,我們不難看出來:對象A獲得依賴對象B的過程,由主動行為變?yōu)榱吮粍有袨椋刂茩囝嵉惯^來了违寞,這就是“控制反轉”這個名稱的由來贞瞒。
控制反轉不只是軟件工程的理論偶房,在生活中我們也有用到這種思想。再舉一個現(xiàn)實生活的例子:
海爾公司作為一個電器制商需要把自己的商品分銷到全國各地军浆,但是發(fā)現(xiàn)棕洋,不同的分銷渠道有不同的玩法,于是派出了各種銷售代表玩不同的玩法乒融,隨著渠道越來越多掰盘,發(fā)現(xiàn),每增加一個渠道就要新增一批人和一個新的流程簇抵,嚴重耦合并依賴各渠道商的玩法庆杜。實在受不了了,于是制定業(yè)務標準碟摆,開發(fā)分銷信息化系統(tǒng)晃财,只有符合這個標準的渠道商才能成為海爾的分銷商。讓各個渠道商反過來依賴自己標準典蜕。反轉了控制断盛,倒置了依賴。
我們把海爾和分銷商當作軟件對象愉舔,分銷信息化系統(tǒng)當作IOC容器钢猛,可以發(fā)現(xiàn),在沒有IOC容器之前轩缤,分銷商就像圖1中的齒輪一樣命迈,增加一個齒輪就要增加多種依賴在其他齒輪上,勢必導致系統(tǒng)越來越復雜火的。開發(fā)分銷系統(tǒng)之后壶愤,所有分銷商只依賴分銷系統(tǒng),就像圖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();
}
}
仔細看這段代碼我們會發(fā)現(xiàn)存在一些問題:
- 如果現(xiàn)在要改變 father 生成方式,如需要用new Father(String name)初始化 father宵统,需要修改 Human 代碼税肪;
- 如果想測試不同 Father 對象對 Human 的影響很困難,因為 father 的初始化被寫死在了 Human 的構造函數(shù)中;
- 如果new Father()過程非常緩慢益兄,單測時我們希望用已經(jīng)初始化好的 father 對象 Mock 掉這個過程也很困難。
依賴注入
上面將依賴在構造函數(shù)中直接初始化是一種 Hard init 方式箭券,弊端在于兩個類不夠獨立净捅,不方便測試。我們還有另外一種 Init 方式辩块,如下:
public class Human {
...
Father father;
...
public Human(Father father) {
this.father = father;
}
}
上面代碼中蛔六,我們將 father 對象作為構造函數(shù)的一個參數(shù)傳入。在調用 Human 的構造方法之前外部就已經(jīng)初始化好了 Father 對象废亭。像這種非自己主動初始化依賴国章,而通過外部來傳入依賴的方式,我們就稱為依賴注入豆村。
現(xiàn)在我們發(fā)現(xiàn)上面 1 中存在的兩個問題都很好解決了液兽,簡單的說依賴注入主要有兩個好處:
- 解耦,將依賴之間解耦掌动。
- 因為已經(jīng)解耦四啰,所以方便做單元測試,尤其是 Mock 測試粗恢。
控制反轉和依賴注入的關系
我們已經(jīng)分別解釋了控制反轉和依賴注入的概念柑晒。有些人會把控制反轉和依賴注入等同,但實際上它們有著本質上的不同眷射。
- 控制反轉是一種思想
- 依賴注入是一種設計模式
IoC框架使用依賴注入作為實現(xiàn)控制反轉的方式匙赞,但是控制反轉還有其他的實現(xiàn)方式,例如說ServiceLocator妖碉,所以不能將控制反轉和依賴注入等同涌庭。
Spring中的依賴注入
上面我們提到,依賴注入是實現(xiàn)控制反轉的一種方式嗅绸。下面我們結合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)建依賴蚀狰。那么問題來了愉昆,誰把依賴傳給他們,也就是說誰負責創(chuàng)建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容器在啟動的時候會根據(jù)配置生成bean實例关贵。具體的配置語法參考Spring文檔。這里只要知道IoC容器會根據(jù)配置創(chuàng)建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());
}
1. 根據(jù)配置生成ApplicationContext势篡,即IoC容器翩肌。
2. 從容器中獲取MovieLister的實例。
總結
1. 控制反轉是一種在軟件工程中解耦合的思想禁悠,調用類只依賴接口念祭,而不依賴具體的實現(xiàn)類,減少了耦合碍侦×焕ぃ控制權交給了容器,在運行的時候才由容器決定將具體的實現(xiàn)動態(tài)的“注入”到調用類的對象中瓷产。
2. 依賴注入是一種設計模式站玄,可以作為控制反轉的一種實現(xiàn)方式。依賴注入就是將實例變量傳入到一個對象中去(Dependency injection means giving an object its instance variables)濒旦。
3. 通過IoC框架株旷,類A依賴類B的強耦合關系可以在運行時通過容器建立,也就是說把創(chuàng)建B實例的工作移交給容器尔邓,類A只管使用就可以晾剖。
下面的是參考自知乎。
要了解控制反轉( Inversion of Control ), 我覺得有必要先了解軟件設計的一個重要思想:依賴倒置原則(Dependency Inversion Principle )梯嗽。
什么是依賴倒置原則齿尽?假設我們設計一輛汽車:先設計輪子,然后根據(jù)輪子大小設計底盤灯节,接著根據(jù)底盤設計車身循头,最后根據(jù)車身設計好整個汽車绵估。這里就出現(xiàn)了一個“依賴”關系:汽車依賴車身,車身依賴底盤卡骂,底盤依賴輪子国裳。
這樣的設計看起來沒問題,但是可維護性卻很低偿警。假設設計完工之后躏救,上司卻突然說根據(jù)市場需求的變動,要我們把車子的輪子設計都改大一碼螟蒸。這下我們就蛋疼了:因為我們是根據(jù)輪子的尺寸設計的底盤,輪子的尺寸一改崩掘,底盤的設計就得修改七嫌;同樣因為我們是根據(jù)底盤設計的車身,那么車身也得改苞慢,同理汽車設計也得改——整個設計幾乎都得改诵原!
我們現(xiàn)在換一種思路。我們先設計汽車的大概樣子挽放,然后根據(jù)汽車的樣子來設計車身绍赛,根據(jù)車身來設計底盤,最后根據(jù)底盤來設計輪子辑畦。這時候吗蚌,依賴關系就倒置過來了:輪子依賴底盤, 底盤依賴車身纯出, 車身依賴汽車蚯妇。
這時候,上司再說要改動輪子的設計暂筝,我們就只需要改動輪子的設計箩言,而不需要動底盤,車身焕襟,汽車的設計了陨收。
這就是依賴倒置原則——把原本的高層建筑依賴底層建筑“倒置”過來,變成底層建筑依賴高層建筑鸵赖。高層建筑決定需要什么务漩,底層去實現(xiàn)這樣的需求,但是高層并不用管底層是怎么實現(xiàn)的卫漫。這樣就不會出現(xiàn)前面的“牽一發(fā)動全身”的情況菲饼。
**控制反轉(Inversion of Control) **就是依賴倒置原則的一種代碼設計的思路。具體采用的方法就是所謂的依賴注入(Dependency Injection)列赎。其實這些概念初次接觸都會感到云里霧里的宏悦。說穿了镐确,這幾種概念的關系大概如下:
為了理解這幾個概念,我們還是用上面汽車的例子饼煞。只不過這次換成代碼源葫。我們先定義四個Class,車砖瞧,車身息堂,底盤,輪胎块促。然后初始化這輛車荣堰,最后跑這輛車。代碼結構如下:
這樣竭翠,就相當于上面第一個例子振坚,上層建筑依賴下層建筑——每一個類的構造函數(shù)都直接調用了底層代碼的構造函數(shù)。假設我們需要改動一下輪胎(Tire)類斋扰,把它的尺寸變成動態(tài)的渡八,而不是一直都是30。我們需要這樣改:
由于我們修改了輪胎的定義传货,為了讓整個程序正常運行屎鳍,我們需要做以下改動:
由此我們可以看到,僅僅是為了修改輪胎的構造函數(shù)问裕,這種設計卻需要修改整個上層所有類的構造函數(shù)逮壁!在軟件工程中,這樣的設計幾乎是不可維護的——在實際工程項目中僻澎,有的類可能會是幾千個類的底層貌踏,如果每次修改這個類,我們都要修改所有以它作為依賴的類窟勃,那軟件的維護成本就太高了祖乳。
所以我們需要進行控制反轉(IoC),及上層控制下層秉氧,而不是下層控制著上層眷昆。我們用依賴注入(Dependency Injection)這種方式來實現(xiàn)控制反轉。所謂依賴注入汁咏,就是把底層類作為參數(shù)傳入上層類亚斋,實現(xiàn)上層類對下層類的“控制”。這里我們用構造方法傳遞的依賴注入方式重新寫車類的定義:
這里我們再把輪胎尺寸變成動態(tài)的攘滩,同樣為了讓整個系統(tǒng)順利運行帅刊,我們需要做如下修改:
看到?jīng)]?這里我只需要修改輪胎類就行了漂问,不用修改其他任何上層類赖瞒。這顯然是更容易維護的代碼女揭。不僅如此,在實際的工程中栏饮,這種設計模式還有利于不同組的協(xié)同合作和單元測試:比如開發(fā)這四個類的分別是四個不同的組吧兔,那么只要定義好了接口,四個不同的組可以同時進行開發(fā)而不相互受限制袍嬉;而對于單元測試境蔼,如果我們要寫Car類的單元測試,就只需要Mock一下Framework類傳入Car就行了伺通,而不用把Framework, Bottom, Tire全部new一遍再來構造Car箍土。
這里我們是采用的構造函數(shù)傳入的方式進行的依賴注入。其實還有另外兩種方法:Setter傳遞和接口傳遞罐监。這里就不多講了涮帘,核心思路都是一樣的,都是為了實現(xiàn)控制反轉笑诅。
看到這里你應該能理解什么控制反轉和依賴注入了。那什么是控制反轉容器(IoC Container)呢疮鲫?其實上面的例子中吆你,對車類進行初始化的那段代碼發(fā)生的地方,就是控制反轉容器俊犯。
顯然你也應該觀察到了妇多,因為采用了依賴注入,在初始化的過程中就不可避免的會寫大量的new燕侠。這里IoC容器就解決了這個問題者祖。這個容器可以自動對你的代碼進行初始化,你只需要維護一個Configuration(可以是xml可以是一段代碼)绢彤,而不用每次初始化一輛車都要親手去寫那一大段初始化的代碼七问。這是引入IoC Container的第一個好處。
IoC Container的第二個好處是:我們在創(chuàng)建實例的時候不需要了解其中的細節(jié)茫舶。在上面的例子中械巡,我們自己手動創(chuàng)建一個車instance時候,是從底層往上層new的:
這個過程中饶氏,我們需要了解整個Car/Framework/Bottom/Tire類構造函數(shù)是怎么定義的讥耗,才能一步一步new/注入。
而IoC Container在進行這個工作的時候是反過來的疹启,它先從最上層開始往下找依賴關系古程,到達最底層之后再往上一步一步new(有點像深度優(yōu)先遍歷):
這里IoC Container可以直接隱藏具體的創(chuàng)建實例的細節(jié),在我們來看它就像一個工廠:
我們就像是工廠的客戶喊崖。我們只需要向工廠請求一個Car實例挣磨,然后它就給我們按照Config創(chuàng)建了一個Car實例雇逞。我們完全不用管這個Car實例是怎么一步一步被創(chuàng)建出來。
實際項目中趋急,有的Service Class可能是十年前寫的喝峦,有幾百個類作為它的底層。假設我們新寫的一個API需要實例化這個Service呜达,我們總不可能回頭去搞清楚這幾百個類的構造函數(shù)吧谣蠢?IoC Container的這個特性就很完美的解決了這類問題——因為這個架構要求你在寫class的時候需要寫相應的Config文件,所以你要初始化很久以前的Service類的時候查近,前人都已經(jīng)寫好了Config文件眉踱,你直接在需要用的地方注入這個Service就可以了。這大大增加了項目的可維護性且降低了開發(fā)難度霜威。