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

轉載鏈接: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)中耦合的對象。

圖1:軟件系統(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)具有依賴關系的對象之間的解耦驹尼。

圖2:IoC解耦過程

由于引進了中間位置的“第三方”,也就是IOC容器庞呕,使得A新翎、B、C住练、D這4個對象沒有了耦合關系地啰,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC容器讲逛,所以亏吝,IOC容器成了整個系統(tǒng)的關鍵核心,它起到了一種類似“粘合劑”的作用盏混,把系統(tǒng)中的所有對象粘合在一起發(fā)揮作用蔚鸥,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯(lián)系许赃,這就是有人把IOC容器比喻成“粘合劑”的由來止喷。
我們再來看看,控制反轉(IOC)到底為什么要起這么個名字图焰?我們來對比一下:

  1. 軟件系統(tǒng)在沒有引入IOC容器之前启盛,如圖1所示,對象A依賴于對象B技羔,那么對象A在初始化或者運行到某一點的時候僵闯,自己必須主動去創(chuàng)建對象B或者使用已經(jīng)創(chuàng)建的對象B。無論是創(chuàng)建還是使用對象B藤滥,控制權都在自己手上鳖粟。

  2. 軟件系統(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)存在一些問題:

  1. 如果現(xiàn)在要改變 father 生成方式,如需要用new Father(String name)初始化 father宵统,需要修改 Human 代碼税肪;
  2. 如果想測試不同 Father 對象對 Human 的影響很困難,因為 father 的初始化被寫死在了 Human 的構造函數(shù)中;
  3. 如果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 中存在的兩個問題都很好解決了液兽,簡單的說依賴注入主要有兩個好處:

  1. 解耦,將依賴之間解耦掌动。
  2. 因為已經(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)了一個“依賴”關系:汽車依賴車身,車身依賴底盤卡骂,底盤依賴輪子国裳。

image

這樣的設計看起來沒問題,但是可維護性卻很低偿警。假設設計完工之后躏救,上司卻突然說根據(jù)市場需求的變動,要我們把車子的輪子設計都改大一碼螟蒸。這下我們就蛋疼了:因為我們是根據(jù)輪子的尺寸設計的底盤,輪子的尺寸一改崩掘,底盤的設計就得修改七嫌;同樣因為我們是根據(jù)底盤設計的車身,那么車身也得改苞慢,同理汽車設計也得改——整個設計幾乎都得改诵原!

我們現(xiàn)在換一種思路。我們先設計汽車的大概樣子挽放,然后根據(jù)汽車的樣子來設計車身绍赛,根據(jù)車身來設計底盤,最后根據(jù)底盤來設計輪子辑畦。這時候吗蚌,依賴關系就倒置過來了:輪子依賴底盤, 底盤依賴車身纯出, 車身依賴汽車蚯妇。

image

這時候,上司再說要改動輪子的設計暂筝,我們就只需要改動輪子的設計箩言,而不需要動底盤,車身焕襟,汽車的設計了陨收。

這就是依賴倒置原則——把原本的高層建筑依賴底層建筑“倒置”過來,變成底層建筑依賴高層建筑鸵赖。高層建筑決定需要什么务漩,底層去實現(xiàn)這樣的需求,但是高層并不用管底層是怎么實現(xiàn)的卫漫。這樣就不會出現(xiàn)前面的“牽一發(fā)動全身”的情況菲饼。

**控制反轉(Inversion of Control) **就是依賴倒置原則的一種代碼設計的思路。具體采用的方法就是所謂的依賴注入(Dependency Injection)列赎。其實這些概念初次接觸都會感到云里霧里的宏悦。說穿了镐确,這幾種概念的關系大概如下:

image

為了理解這幾個概念,我們還是用上面汽車的例子饼煞。只不過這次換成代碼源葫。我們先定義四個Class,車砖瞧,車身息堂,底盤,輪胎块促。然后初始化這輛車荣堰,最后跑這輛車。代碼結構如下:

image

這樣竭翠,就相當于上面第一個例子振坚,上層建筑依賴下層建筑——每一個類的構造函數(shù)都直接調用了底層代碼的構造函數(shù)。假設我們需要改動一下輪胎(Tire)類斋扰,把它的尺寸變成動態(tài)的渡八,而不是一直都是30。我們需要這樣改:

image

由于我們修改了輪胎的定義传货,為了讓整個程序正常運行屎鳍,我們需要做以下改動:

image

由此我們可以看到,僅僅是為了修改輪胎的構造函數(shù)问裕,這種設計卻需要修改整個上層所有類的構造函數(shù)逮壁!在軟件工程中,這樣的設計幾乎是不可維護的——在實際工程項目中僻澎,有的類可能會是幾千個類的底層貌踏,如果每次修改這個類,我們都要修改所有以它作為依賴的類窟勃,那軟件的維護成本就太高了祖乳。

所以我們需要進行控制反轉(IoC),及上層控制下層秉氧,而不是下層控制著上層眷昆。我們用依賴注入(Dependency Injection)這種方式來實現(xiàn)控制反轉。所謂依賴注入汁咏,就是把底層類作為參數(shù)傳入上層類亚斋,實現(xiàn)上層類對下層類的“控制”。這里我們用構造方法傳遞的依賴注入方式重新寫車類的定義:

image

這里我們再把輪胎尺寸變成動態(tài)的攘滩,同樣為了讓整個系統(tǒng)順利運行帅刊,我們需要做如下修改:

image

看到?jīng)]?這里我只需要修改輪胎類就行了漂问,不用修改其他任何上層類赖瞒。這顯然是更容易維護的代碼女揭。不僅如此,在實際的工程中栏饮,這種設計模式還有利于不同組的協(xié)同合作和單元測試:比如開發(fā)這四個類的分別是四個不同的組吧兔,那么只要定義好了接口,四個不同的組可以同時進行開發(fā)而不相互受限制袍嬉;而對于單元測試境蔼,如果我們要寫Car類的單元測試,就只需要Mock一下Framework類傳入Car就行了伺通,而不用把Framework, Bottom, Tire全部new一遍再來構造Car箍土。

這里我們是采用的構造函數(shù)傳入的方式進行的依賴注入。其實還有另外兩種方法:Setter傳遞和接口傳遞罐监。這里就不多講了涮帘,核心思路都是一樣的,都是為了實現(xiàn)控制反轉笑诅。

image

看到這里你應該能理解什么控制反轉和依賴注入了。那什么是控制反轉容器(IoC Container)呢疮鲫?其實上面的例子中吆你,對車類進行初始化的那段代碼發(fā)生的地方,就是控制反轉容器俊犯。

image

顯然你也應該觀察到了妇多,因為采用了依賴注入,在初始化的過程中就不可避免的會寫大量的new燕侠。這里IoC容器就解決了這個問題者祖。這個容器可以自動對你的代碼進行初始化,你只需要維護一個Configuration(可以是xml可以是一段代碼)绢彤,而不用每次初始化一輛車都要親手去寫那一大段初始化的代碼七问。這是引入IoC Container的第一個好處。

IoC Container的第二個好處是:我們在創(chuàng)建實例的時候不需要了解其中的細節(jié)茫舶。在上面的例子中械巡,我們自己手動創(chuàng)建一個車instance時候,是從底層往上層new的:

image

這個過程中饶氏,我們需要了解整個Car/Framework/Bottom/Tire類構造函數(shù)是怎么定義的讥耗,才能一步一步new/注入。

而IoC Container在進行這個工作的時候是反過來的疹启,它先從最上層開始往下找依賴關系古程,到達最底層之后再往上一步一步new(有點像深度優(yōu)先遍歷):

image

這里IoC Container可以直接隱藏具體的創(chuàng)建實例的細節(jié),在我們來看它就像一個工廠:

image

我們就像是工廠的客戶喊崖。我們只需要向工廠請求一個Car實例挣磨,然后它就給我們按照Config創(chuàng)建了一個Car實例雇逞。我們完全不用管這個Car實例是怎么一步一步被創(chuàng)建出來。

實際項目中趋急,有的Service Class可能是十年前寫的喝峦,有幾百個類作為它的底層。假設我們新寫的一個API需要實例化這個Service呜达,我們總不可能回頭去搞清楚這幾百個類的構造函數(shù)吧谣蠢?IoC Container的這個特性就很完美的解決了這類問題——因為這個架構要求你在寫class的時候需要寫相應的Config文件,所以你要初始化很久以前的Service類的時候查近,前人都已經(jīng)寫好了Config文件眉踱,你直接在需要用的地方注入這個Service就可以了。這大大增加了項目的可維護性且降低了開發(fā)難度霜威。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末谈喳,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子戈泼,更是在濱河造成了極大的恐慌婿禽,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件大猛,死亡現(xiàn)場離奇詭異扭倾,居然都是意外死亡,警方通過查閱死者的電腦和手機挽绩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門膛壹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人唉堪,你說我怎么就攤上這事模聋。” “怎么了唠亚?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵链方,是天一觀的道長。 經(jīng)常有香客問我趾撵,道長侄柔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任占调,我火速辦了婚禮暂题,結果婚禮上,老公的妹妹穿的比我還像新娘究珊。我一直安慰自己薪者,他們只是感情好,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布剿涮。 她就那樣靜靜地躺著言津,像睡著了一般攻人。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悬槽,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天怀吻,我揣著相機與錄音,去河邊找鬼初婆。 笑死蓬坡,一個胖子當著我的面吹牛,可吹牛的內容都是我干的磅叛。 我是一名探鬼主播屑咳,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弊琴!你這毒婦竟也來了兆龙?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤敲董,失蹤者是張志新(化名)和其女友劉穎紫皇,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腋寨,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡坝橡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了精置。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡锣杂,死狀恐怖脂倦,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情元莫,我是刑警寧澤赖阻,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站踱蠢,受9級特大地震影響火欧,放射性物質發(fā)生泄漏。R本人自食惡果不足惜茎截,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一苇侵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧企锌,春花似錦榆浓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烘浦。三九已至,卻和暖如春萍鲸,著一層夾襖步出監(jiān)牢的瞬間闷叉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工脊阴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留握侧,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓蹬叭,卻偏偏與公主長得像藕咏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子秽五,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容