你確定懂?徹底搞懂 控制反轉(IoC Inversion of Control )與依賴注入(DI Dependency Inversion Principle )
說明
Spring框架,核心就是IoC容器。要掌握Spring框架,就必須要理解控制反轉的思想以及依賴注入的實現方式。下面,我們將圍繞下面幾個問題來探討控制反轉與依賴注入的關係以及在Spring中如何應用。
什麼是控制反轉?
什麼是依賴注入?
它們之間有什麼關係?
如何在Spring框架中應用依賴注入?
1. 控制反轉
在討論控制反轉之前,我們先來看看軟體系統中耦合的物件。
圖1:軟體系統中耦合的物件
從圖中可以看到,軟體中的物件就像齒輪一樣,協同工作,但是互相耦合,一個零件不能正常工作,整個系統就崩潰了。這是一個強耦合的系統。齒輪組中齒輪之間的齧合關係,與軟體系統中物件之間的耦合關係非常相似。物件之間的耦合關係是無法避免的,也是必要的,這是協同工作的基礎。現在,伴隨著工業級應用的規模越來越龐大,物件之間的依賴關係也越來越複雜,經常會出現物件之間的多重依賴性關係,因此,架構師和設計師對於系統的分析和設計,將面臨更大的挑戰。物件之間耦合度過高的系統,必然會出現牽一髮而動全身的情形。
為了解決物件間耦合度過高的問題,軟體專家Michael Mattson提出了IoC理論,用來實現物件之間的“解耦”。
控制反轉(Inversion of Control)是一種是物件導向程式設計中的一種設計原則,用來減低計算機程式碼之間的耦合度。其基本思想是:藉助於“第三方”實現具有依賴關係的物件之間的解耦。
圖2: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顯示那樣,可以很方便的增加和刪除齒輪上去。
2. 依賴注入
依賴注入就是將例項變數傳入到一個物件中去(Dependency injection means giving an object its instance variables)。
2.1 什麼是依賴
如果在 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 掉這個過程也很困難。
2.2 依賴注入
上面將依賴在建構函式中直接初始化是一種 Hard init 方式,弊端在於兩個類不夠獨立,不方便測試。我們還有另外一種 Init 方式,如下:
public class Human {
...
Father father;
...
public Human(Father father) {
this.father = father;
}
}
上面程式碼中,我們將 father 物件作為建構函式的一個引數傳入。在呼叫 Human 的構造方法之前外部就已經初始化好了 Father 物件。像這種非自己主動初始化依賴,而通過外部來傳入依賴的方式,我們就稱為依賴注入。
現在我們發現上面 1 中存在的兩個問題都很好解決了,簡單的說依賴注入主要有兩個好處:
- 解耦,將依賴之間解耦。
- 因為已經解耦,所以方便做單元測試,尤其是 Mock 測試。
2.3 控制反轉和依賴注入的關係
我們已經分別解釋了控制反轉和依賴注入的概念。有些人會把控制反轉和依賴注入等同,但實際上它們有著本質上的不同。
- 控制反轉是一種思想
- 依賴注入是一種設計模式
IoC框架使用依賴注入作為實現控制反轉的方式,但是控制反轉還有其他的實現方式,例如說ServiceLocator,所以不能將控制反轉和依賴注入等同。
2.4 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的例項。
2.5 總結
-
控制反轉是一種在軟體工程中解耦合的思想,呼叫類只依賴介面,而不依賴具體的實現類,減少了耦合。控制權交給了容器,在執行的時候才由容器決定將具體的實現動態的“注入”到呼叫類的物件中。
-
依賴注入是一種設計模式,可以作為控制反轉的一種實現方式。依賴注入就是將例項變數傳入到一個物件中去(Dependency injection means giving an object its instance variables)。
-
通過IoC框架,類A依賴類B的強耦合關係可以在執行時通過容器建立,也就是說把建立B例項的工作移交給容器,類A只管使用就可以。
3 汽車與輪子的關係 理解IoC 和 DI
要了解控制反轉( Inversion of Control ), 我覺得有必要先了解軟體設計的一個重要思想:依賴倒置原則(Dependency Inversion Principle )。
3.1 什麼是依賴倒置原則?
假設我們設計一輛汽車:先設計輪子,然後根據輪子大小設計底盤,接著根據底盤設計車身,最後根據車身設計好整個汽車。這裡就出現了一個“依賴”關係:汽車依賴車身,車身依賴底盤,底盤依賴輪子。
這樣的設計看起來沒問題,但是可維護性卻很低。假設設計完工之後,上司卻突然說根據市場需求的變動,要我們把車子的輪子設計都改大一碼。這下我們就蛋疼了:因為我們是根據輪子的尺寸設計的底盤,輪子的尺寸一改,底盤的設計就得修改;同樣因為我們是根據底盤設計的車身,那麼車身也得改,同理汽車設計也得改——整個設計幾乎都得改!
我們現在換一種思路。我們先設計汽車的大概樣子,然後根據汽車的樣子來設計車身,根據車身來設計底盤,最後根據底盤來設計輪子。這時候,依賴關係就倒置過來了:輪子依賴底盤, 底盤依賴車身, 車身依賴汽車。
這時候,上司再說要改動輪子的設計,我們就只需要改動輪子的設計,而不需要動底盤,車身,汽車的設計了。
這就是依賴倒置原則——把原本的高層建築依賴底層建築“倒置”過來,變成底層建築依賴高層建築。高層建築決定需要什麼,底層去實現這樣的需求,但是高層並不用管底層是怎麼實現的。這樣就不會出現前面的“牽一髮動全身”的情況。
3.2 控制反轉 Inversion of Control
就是依賴倒置原則的一種程式碼設計的思路。具體採用的方法就是所謂的依賴注入(Dependency Injection)。其實這些概念初次接觸都會感到雲裡霧裡的。說穿了,這幾種概念的關係大概如下:
為了理解這幾個概念,我們還是用上面汽車的例子。只不過這次換成程式碼。我們先定義四個Class,車,車身,底盤,輪胎。然後初始化這輛車,最後跑這輛車。程式碼結構如下:
這樣,就相當於上面第一個例子,上層建築依賴下層建築——每一個類的建構函式都直接呼叫了底層程式碼的建構函式。假設我們需要改動一下輪胎(Tire)類,把它的尺寸變成動態的,而不是一直都是30。我們需要這樣改:
由於我們修改了輪胎的定義,為了讓整個程式正常執行,我們需要做以下改動:
由此我們可以看到,僅僅是為了修改輪胎的建構函式,這種設計卻需要修改整個上層所有類的建構函式!在軟體工程中,這樣的設計幾乎是不可維護的——在實際工程專案中,有的類可能會是幾千個類的底層,如果每次修改這個類,我們都要修改所有以它作為依賴的類,那軟體的維護成本就太高了。
所以我們需要進行控制反轉(IoC),及上層控制下層,而不是下層控制著上層。我們用依賴注入(Dependency Injection)這種方式來實現控制反轉。所謂依賴注入,就是把底層類作為引數傳入上層類,實現上層類對下層類的“控制”。這裡我們用構造方法傳遞的依賴注入方式重新寫車類的定義:
這裡我們再把輪胎尺寸變成動態的,同樣為了讓整個系統順利執行,我們需要做如下修改:
看到沒?這裡我只需要修改輪胎類就行了,不用修改其他任何上層類。這顯然是更容易維護的程式碼。不僅如此,在實際的工程中,這種設計模式還有利於不同組的協同合作和單元測試:比如開發這四個類的分別是四個不同的組,那麼只要定義好了介面,四個不同的組可以同時進行開發而不相互受限制;而對於單元測試,如果我們要寫Car類的單元測試,就只需要Mock一下Framework類傳入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再來構造Car。
3.3 Setter傳遞和介面傳遞 的依賴注入
這裡我們是採用的建構函式傳入的方式進行的依賴注入。其實還有另外兩種方法:Setter傳遞和介面傳遞。這裡就不多講了,核心思路都是一樣的,都是為了實現控制反轉。
看到這裡你應該能理解什麼控制反轉和依賴注入了。
4 控制反轉容器(IoC Container)
那什麼是控制反轉容器(IoC Container)呢?其實上面的例子中,對車類進行初始化的那段程式碼發生的地方,就是控制反轉容器。
顯然你也應該觀察到了,因為採用了依賴注入,在初始化的過程中就不可避免的會寫大量的new。這裡IoC容器就解決了這個問題。這個容器可以自動對你的程式碼進行初始化,你只需要維護一個Configuration(可以是xml可以是一段程式碼),而不用每次初始化一輛車都要親手去寫那一大段初始化的程式碼。這是引入IoC Container的第一個好處。
IoC Container的第二個好處是:我們在建立例項的時候不需要了解其中的細節。在上面的例子中,我們自己手動建立一個車instance時候,是從底層往上層new的:
這個過程中,我們需要了解整個Car/Framework/Bottom/Tire類建構函式是怎麼定義的,才能一步一步new/注入。
而IoC Container在進行這個工作的時候是反過來的,它先從最上層開始往下找依賴關係,到達最底層之後再往上一步一步new(有點像深度優先遍歷):
這裡IoC Container可以直接隱藏具體的建立例項的細節,在我們來看它就像一個工廠:
我們就像是工廠的客戶。我們只需要向工廠請求一個Car例項,然後它就給我們按照Config建立了一個Car例項。我們完全不用管這個Car例項是怎麼一步一步被建立出來。
實際專案中,有的Service Class可能是十年前寫的,有幾百個類作為它的底層。假設我們新寫的一個API需要例項化這個Service,我們總不可能回頭去搞清楚這幾百個類的建構函式吧?IoC Container的這個特性就很完美的解決了這類問題——因為這個架構要求你在寫class的時候需要寫相應的Config檔案,所以你要初始化很久以前的Service類的時候,前人都已經寫好了Config檔案,你直接在需要用的地方注入這個Service就可以了。這大大增加了專案的可維護性且降低了開發難度。
5 解耦的思路
消費者X需要消費類Y來完成某項工作。那都是自然而然的,但是X真的需要知道它使用Y嗎?
知道X使用具有Y的行為,方法,屬性等的東西而又不知道是誰真正實現了行為,這還不夠嗎?
通過提取X在Y中使用的行為的抽象定義(如下圖I所示),並讓消費者X使用該例項代替Y,它可以繼續執行其操作而不必瞭解有關Y的細節。
在上面的圖示中,Y實現了I,而X使用了I的例項。儘管X仍然很可能仍使用Y,但有趣的是X並不知道這一點。它只知道它使用實現I的東西。
參考
https://www.zhihu.com/question/23277575
https://www.jianshu.com/p/07af9dbbbc4b
https://martinfowler.com/articles/injection.html
http://joelabrahamsson.com/inversion-of-control-an-introduction-with-examples-in-net/
相關文章
- 控制反轉(IoC)與依賴注入(DI)依賴注入
- Java:控制反轉(IoC)與依賴注入(DI)Java依賴注入
- 控制反轉與依賴注入(IOC和DI)依賴注入
- .NET IoC模式依賴反轉(DIP)、控制反轉(Ioc)、依賴注入(DI)模式依賴注入
- 8.(轉)控制反轉(IoC)與依賴注入(DI)依賴注入
- PHP 控制反轉(IoC) 和 依賴注入(DI)PHP依賴注入
- PHP 控制反轉(IOC)和依賴注入(DI)PHP依賴注入
- 依賴倒置原則(Dependence Inversion Principle)
- 深入理解IoC(控制反轉)、DI(依賴注入)依賴注入
- 解構控制反轉(IoC)和依賴注入(DI)依賴注入
- 理解Spring中依賴注入(DI)與控制反轉(IoC)Spring依賴注入
- 深入理解控制反轉(IoC)和依賴注入(DI)依賴注入
- php實現依賴注入(DI)和控制反轉(IOC)PHP依賴注入
- Spring框架之IOC/DI(控制反轉/依賴注入)Spring框架依賴注入
- 控制反轉(IOC)與依賴注入(DI)模式解析及實踐依賴注入模式
- Spring系列第二講 控制反轉(IoC)與依賴注入(DI),晦澀難懂麼?Spring依賴注入
- OOD、DIP、IOC、DI、依賴注入容器(即 控制反轉容器,IOC Container)依賴注入AI
- Laravel Inversion of Control (控制反轉) 概念簡介Laravel
- 深入探討控制反轉(IOC)與依賴注入(DI)模式原理與應用實踐依賴注入模式
- 淺析依賴倒轉、控制反轉、IoC 容器、依賴注入。依賴注入
- 深入理解spring容器中的控制反轉(IOC)和依賴注入(DI)Spring依賴注入
- Spring4學習(一)IoC控制反轉也稱為DI依賴注入Spring依賴注入
- .Net DI(Dependency Injection)依賴注入機制依賴注入
- 依賴注入和控制反轉依賴注入
- 什麼是控制反轉(IOC)?什麼是依賴注入?依賴注入
- 一文徹底弄懂Spring IOC 依賴注入Spring依賴注入
- 依賴倒置(DIP)與依賴注入(DI)依賴注入
- CommunityToolkit.Mvvm8.1 IOC依賴注入控制反轉(5)UnityMVVM依賴注入
- 前端理解依賴注入(控制反轉)前端依賴注入
- .net core 原始碼分析(9) 依賴注入(DI)-Dependency Injection原始碼依賴注入
- Spring 控制反轉和依賴注入Spring依賴注入
- 【Spring】Spring依賴注入與控制反轉理解Spring依賴注入
- java 控制反轉和依賴注入的理解Java依賴注入
- 控制反轉,依賴注入,依賴倒置傻傻分不清楚?依賴注入
- Spring 依賴注入 DISpring依賴注入
- 面試官:你是如何理解Java中依賴倒置和依賴注入以及控制反轉的?面試Java依賴注入
- 依賴倒置、依賴注入和控制反轉傻傻分不清楚?依賴注入
- 一張圖徹底搞懂Spring迴圈依賴Spring