天天看點

控制反轉

控制反轉(Inversion of Control,英文縮寫為IoC)是一個重要的面向對象程式設計的法則來削減計算機程式的耦合問題,也是輕量級的Spring架構的核心。 控制反轉一般分為兩種類型,依賴注入(Dependency Injection,簡稱DI)和依賴查找(Dependency Lookup)。依賴注入應用比較廣泛。

編輯

早在2004年,Martin Fowler就提出了“哪些方面的控制被反轉了?”這個問題。他總結出是依賴對象的獲得被反轉了。基于這個結論,他為控制反轉創造了一個更好的名字:依賴注入。許多非凡的應用(比HelloWorld.java更加優美,更加複雜)都是由兩個或是更多的類通過彼此的合作來實作業務邏輯,這使得每個對象都需要與其合作的對象(也就是它所依賴的對象)的引用。如果這個擷取過程要靠自身實作,那麼如你所見,這将導緻代碼高度耦合并且難以測試。

IoC 亦稱為 “依賴倒置原理”("Dependency Inversion Principle")。差不多所有架構都使用了“倒置注入(Fowler 2004)技巧,這可說是IoC原理的一項應用。SmallTalk,C++, Java 或.NET 等各種面向對象程式語言的程式員已使用了這些原理。

控制反轉是Spring架構的核心。

應用控制反轉,對象在被建立的時候,由一個調控系統内所有對象的外界實體将其所依賴的對象的引用傳遞給它。也可以說,依賴被注入到對象中。是以,控制反轉是,關于一個對象如何擷取他所依賴的對象的引用,這個責任的反轉。

IoC可以認為是一種全新的設計模式,但是理論和時間成熟相對較晚,并沒有包含在GoF中。

Interface Driven Design接口驅動,接口驅動有很多好處,可以提供不同靈活的子類實作,增加代碼穩定和健壯性等等,但是接口一定是需要實作的,也就是如下語句遲早要執行:AInterface a = new AInterfaceImp(); 這樣一來,耦合關系就産生了,如:

<col>

1

2

3

4

5

6

7

8

9

10

11

​<code>​classA​</code>​

​<code>​{​</code>​

​<code>​AInterface a;​</code>​

​<code>​A(){}​</code>​

​<code>​AMethod()​</code>​​<code>​//一個方法​</code>​

​<code>​a = ​</code>​​<code>​new​</code>​ ​<code>​AInterfaceImp();​</code>​

​<code>​}​</code>​

Class A與AInterfaceImp就是依賴關系,如果想使用AInterface的另外一個實作就需要更改代碼了。當然我們可以建立一個Factory來根據條件生成想要的AInterface的具體實作,即:

12

13

14

15

16

17

18

​<code>​InterfaceImplFactory​</code>​

​<code>​AInterface create(Object condition)​</code>​

​<code>​if​</code>​​<code>​(condition == condA)​</code>​

​<code>​return​</code>​ ​<code>​new​</code>​ ​<code>​AInterfaceImpA();​</code>​

​<code>​else​</code>​ ​<code>​if​</code>​​<code>​(condition == condB)​</code>​

​<code>​return​</code>​ ​<code>​new​</code>​ ​<code>​AInterfaceImpB();​</code>​

​<code>​else​</code>​

​<code>​return​</code>​ ​<code>​new​</code>​ ​<code>​AInterfaceImp();​</code>​

表面上是在一定程度上緩解了以上問題,但實質上這種代碼耦合并沒有改變。通過IoC模式可以徹底解決這種耦合,它把耦合從代碼中移出去,放到統一的XML 檔案中,通過一個容器在需要的時候把這個依賴關系形成,即把需要的接口實作注入到需要它的類中,這可能就是“依賴注入”說法的來源了。

IoC模式,系統中通過引入實作了IoC模式的IoC容器,即可由IoC容器來管理對象的生命周期、依賴關系等,進而使得應用程式的配置和依賴性規範與實際的應用程式代碼分開。其中一個特點就是通過文本的配置檔案進行應用程式元件間互相關系的配置,而不用重新修改并編譯具體的代碼。

目前比較知名的IoC容器有:Pico Container、Avalon 、Spring、JBoss、HiveMind、EJB等。

在上面的幾個IoC容器中,輕量級的有Pico Container、Avalon、Spring、HiveMind等,超重量級的有EJB,而半輕半重的有容器有JBoss,Jdon等。

可以把IoC模式看做是工廠模式的升華,可以把IoC看作是一個大工廠,隻不過這個大工廠裡要生成的對象都是在XML檔案中給出定義的,然後利用Java 的“反射”程式設計,根據XML中給出的類名生成相應的對象。從實作來看,IoC是把以前在工廠方法裡寫死的對象生成代碼,改變為由XML檔案來定義,也就是把工廠和對象生成這兩者獨立分隔開來,目的就是提高靈活性和可維護性。

IoC中最基本的Java技術就是“反射”程式設計。反射又是一個生澀的名詞,通俗的說反射就是根據給出的類名(字元串)來生成對象。這種程式設計方式可以讓對象在生成時才決定要生成哪一種對象。反射的應用是很廣泛的,像Hibernate、Spring中都是用“反射”做為最基本的技術手段。

在過去,反射程式設計方式相對于正常的對象生成方式要慢10幾倍,這也許也是當時為什麼反射技術沒有普遍應用開來的原因。但經SUN改良優化後,反射方式生成對象和通常對象生成方式,速度已經相差不大了(但依然有一倍以上的差距)。

IoC最大的好處是什麼?因為把對象生成放在了XML裡定義,是以當我們需要換一個實作子類将會變成很簡單(一般這樣的對象都是實作于某種接口的),隻要修改XML就可以了,這樣我們甚至可以實作對象的熱插拔(有點像USB接口和SCSI硬碟了)。

IoC最大的缺點是什麼?(1)生成一個對象的步驟變複雜了(事實上操作上還是挺簡單的),對于不習慣這種方式的人,會覺得有些别扭和不直覺。(2)對象生成因為是使用反射程式設計,在效率上有些損耗。但相對于IoC提高的維護性和靈活性來說,這點損耗是微不足道的,除非某對象的生成對效率要求特别高。(3)缺少IDE重構操作的支援,如果在Eclipse要對類改名,那麼你還需要去XML檔案裡手工去改了,這似乎是所有XML方式的缺陷所在。

IOC關注服務(或應用程式部件)是如何定義的以及他們應該如何定位他們依賴的其它服務。通常,通過一個容器或定位架構來獲得定義和定位的分離,容器或定位架構負責:

儲存可用服務的集合

提供一種方式将各種部件與它們依賴的服務綁定在一起

為應用程式代碼提供一種方式來請求已配置的對象(例如,一個所有依賴都滿足的對象), 這種方式可以確定該對象需要的所有相關的服務都可用。

現有的架構實際上使用以下三種基本技術的架構執行服務和部件間的綁定:

類型1 (基于接口): 可服務的對象需要實作一個專門的接口,該接口提供了一個對象,可以重用這個對象查找依賴(其它服務)。早期的容器Excalibur使用這種模式。

類型2 (基于setter): 通過JavaBean的屬性(setter方法)為可服務對象指定服務。HiveMind和Spring采用這種方式。

類型3 (基于構造函數): 通過構造函數的參數為可服務對象指定服務。PicoContainer隻使用這種方式。HiveMind和Spring也使用這種方式。

IoC是一個很大的概念,可以用不同的方式實作。其主要形式有兩種:

◇依賴查找:容器提供回調接口和上下文條件給元件。EJB和Apache Avalon 都使用這種方式。這樣一來,元件就必須使用容器提供的API來查找資源和協作對象,僅有的控制反轉隻展現在那些回調方法上(也就是上面所說的 類型1):容器将調用這些回調方法,進而讓應用代碼獲得相關資源。

◇依賴注入:元件不做定位查詢,隻提供普通的Java方法讓容器去決定依賴關系。容器全權負責的元件的裝配,它會把符合依賴關系的對象通過JavaBean屬性或者構造函數傳遞給需要的對象。通過JavaBean屬性注射依賴關系的做法稱為設值方法注入(Setter Injection);将依賴關系作為構造函數參數傳入的做法稱為構造器注入(Constructor Injection)

實作資料通路層

資料通路層有兩個目标。第一是将資料庫引擎從應用中抽象出來,這樣就可以随時改變資料庫—比方說,從微軟SQL變成Oracle。不過在實踐上很少會這麼做,也沒有足夠的理由未來使用實作資料通路層而進行重構現有應用的努力。[1] 

第二個目标是将資料模型從資料庫實作中抽象出來。這使得資料庫或代碼開源根據需要改變,同時隻會影響主應用的一小部分——資料通路層。這一目标是值得的,為了在現有系統中實作它進行必要的重構。

子產品與接口重構

依賴注入背後的一個核心思想是單一功能原則(single responsibility principle)。該原則指出,每一個對象應該有一個特定的目的,而應用需要利用這一目的的不同部分應當使用合适的對象。這意味着這些對象在系統的任何地方都可以重用。但在現有系統裡面很多時候都不是這樣的。[1] 

随時增加單元測試

把功能封裝到整個對象裡面會導緻自動測試困難或者不可能。将子產品和接口與特定對象隔離,以這種方式重構可以執行更先進的單元測試。按照後面再增加測試的想法繼續重構子產品是誘惑力的,但這是錯誤的。[1] 

使用服務定位器而不是構造注入

實作控制反轉不止一種方法。最常見的辦法是使用構造注入,這需要在對象首次被建立是提供所有的軟體依賴。然而,構造注入要假設整個系統都使用這一模式,這意味着整個系統必須同時進行重構。這很困難、有風險,且耗時。