轉載地址:http://stamen.iteye.com/blog/1489223
引述:IoC(控制反轉:Inverse of Control)是Spring容器的內核,AOP、聲明式事務等功能在此基礎上開花結果。但是IoC這個重要的概念卻比較晦澀隱諱,不容易讓人望文生義,這不能不說是一大遺憾。不過IoC確實包括很多內涵,它涉及代碼解耦、設計模式、代碼優化等問題的考量,我們打算通過一個小例子來說明這個概念。
通過實例理解IoC的概念
賀歲大片在中國已經形成了一個傳統,每到年底總有多部賀歲大片紛至沓來讓人應接不暇。在所有賀歲大片中,張之亮的《墨攻》算是比較出彩的一部。該片講述了戰國時期墨家人革離幫助樑國反抗趙國侵略的個人英雄主義故事,恢宏壯闊、渾雄凝重的歷史場面相當震撼。其中有一個場景:當劉德華所飾演的墨者革離到達樑國都城下,城上樑國守軍問到:“來者何人?”劉德華回答:“墨者革離!”我們不妨通過一個Java類爲這個“城門叩問”的場景進行編劇,並藉此理解IoC的概念:
代碼清單3-1 MoAttack:通過演員安排劇本
- public class MoAttack {
- public void cityGateAsk(){
- //①演員直接侵入劇本
- LiuDeHua ldh = new LiuDeHua();
- ldh.responseAsk("墨者革離!");
- }
- }
我們會發現以上劇本在①處,作爲具體角色飾演者的劉德華直接侵入到劇本中,使劇本和演員直接耦合在一起(圖3-1)。
一個明智的編劇在劇情創作時應圍繞故事的角色進行,而不應考慮角色的具體飾演者,這樣纔可能在劇本投拍時自由地遴選任何適合的演員,而非綁定在劉德華一人身上。通過以上的分析,我們知道需要爲該劇本主人公革離定義一個接口:
代碼清單3-2 MoAttack:引入劇本角色
- public class MoAttack {
- public void cityGateAsk()
- {
- //①引入革離角色接口
- GeLi geli = new LiuDeHua();
- //②通過接口開展劇情
- geli.responseAsk("墨者革離!");
- }
- }
在①處引入了劇本的角色——革離,劇本的情節通過角色展開,在拍攝時角色由演員飾演,如②處所示。因此墨攻、革離、劉德華三者的類圖關係如圖 3 2所示:
可是,從圖3 2中,我們可以看出MoAttack同時依賴於GeLi接口和LiuDeHua類,並沒有達到我們所期望的劇本僅依賴於角色的目的。但是角色最終必須通過具體的演員才能完成拍攝,如何讓LiuDeHua和劇本無關而又能完成GeLi的具體動作呢?當然是在影片投拍時,導演將LiuDeHua安排在GeLi的角色上,導演將劇本、角色、飾演者裝配起來(圖3-3)。
通過引入導演,使劇本和具體飾演者解耦了。對應到軟件中,導演像是一個裝配器,安排演員表演具體的角色。
現在我們可以反過來講解IoC的概念了。IoC(Inverse of Control)的字面意思是控制反轉,它包括兩個內容:
- 其一是控制
- 其二是反轉
那到底是什麼東西的“控制”被“反轉”了呢?對應到前面的例子,“控制”是指選擇GeLi角色扮演者的控制權;“反轉”是指這種控制權從《墨攻》劇本中移除,轉交到導演的手中。對於軟件來說,即是某一接口具體實現類的選擇控制權從調用類中移除,轉交給第三方決定。
因爲IoC確實不夠開門見山,因此業界曾進行了廣泛的討論,最終軟件界的泰斗級人物Martin Fowler提出了DI(依賴注入:Dependency Injection)的概念用以代替IoC,即讓調用類對某一接口實現類的依賴關係由第三方(容器或協作類)注入,以移除調用類對某一接口實現類的依賴。“依賴注入”這個名詞顯然比“控制反轉”直接明瞭、易於理解。
IoC的類型
從注入方法上看,主要可以劃分爲三種類型:構造函數注入、屬性注入和接口注入。Spring支持構造函數注入和屬性注入。下面我們繼續使用以上的例子說明這三種注入方法的區別。
構造函數注入
在構造函數注入中,我們通過調用類的構造函數,將接口實現類通過構造函數變量傳入,如代碼清單3-3所示:
代碼清單3-3 MoAttack:通過構造函數注入革離扮演者
- public class MoAttack {
- private GeLi geli;
- //①注入革離的具體扮演者
- public MoAttack(GeLi geli){
- this.geli = geli;
- }
- public void cityGateAsk(){
- geli.responseAsk("墨者革離!");
- }
- }
MoAttack的構造函數不關心具體是誰扮演革離這個角色,只要在①處傳入的扮演者按劇本要求完成相應的表演即可。角色的具體扮演者由導演來安排,如代碼清單3-4所示:
代碼清單3-4 Director:通過構造函數注入革離扮演者
- public class Director {
- public void direct(){
- //①指定角色的扮演者
- GeLi geli = new LiuDeHua();
- //②注入具體扮演者到劇本中
- MoAttack moAttack = new MoAttack(geli);
- moAttack.cityGateAsk();
- }
- }
在①處,導演安排劉德華飾演革離的角色,並在②處,將劉德華“注入”到墨攻的劇本中,然後開始“城門叩問”劇情的演出工作。
屬性注入
有時,導演會發現,雖然革離是影片《墨攻》的第一主角,但並非每個場景都需要革離的出現,在這種情況下通過構造函數注入相當於每時每刻都在革離的飾演者在場,可見並不妥當,這時可以考慮使用屬性注入。屬性注入可以有選擇地通過Setter方法完成調用類所需依賴的注入,更加靈活方便:
代碼清單3-5 MoAttack:通過Setter方法注入革離扮演者
- public class MoAttack {
- private GeLi geli;
- //①屬性注入方法
- public void setGeli(GeLi geli) {
- this.geli = geli;
- }
- public void cityGateAsk() {
- geli.responseAsk("墨者革離");
- }
- }
MoAttack在①處爲geli屬性提供一個Setter方法,以便讓導演在需要時注入geli的具體扮演者。
代碼清單3-6 Director:通過Setter方法注入革離扮演者
- public class Director {
- public void direct(){
- GeLi geli = new LiuDeHua();
- MoAttack moAttack = new MoAttack();
- //①調用屬性Setter方法注入
- moAttack.setGeli(geli);
- moAttack.cityGateAsk();
- }
- }
和通過構造函數注入革離扮演者不同,在實例化MoAttack劇本時,並未指定任何扮演者,而是在實例化MoAttack後,在需要革離出場時,才調用其setGeli()方法注入扮演者。按照類似的方式,我們還可以分別爲劇本中其他諸如樑王、巷淹中等角色提供注入的Setter方法,這樣,導演就可以根據所拍劇段的不同,注入相應的角色了。
接口注入
將調用類所有依賴注入的方法抽取到一個接口中,調用類通過實現該接口提供相應的注入方法。爲了採取接口注入的方式,必須先聲明一個ActorArrangable接口:
- public interface ActorArrangable {
- void injectGeli(GeLi geli);
- }
然後,MoAttack實現ActorArrangable接口提供具體的實現:
代碼清單3-7 MoAttack:通過接口方法注入革離扮演者
- public class MoAttack implements ActorArrangable {
- private GeLi geli;
- //①實現接口方法
- public void injectGeli (GeLi geli) {
- this.geli = geli;
- }
- public void cityGateAsk() {
- geli.responseAsk("墨者革離");
- }
- }
Director通過ActorArrangable的injectGeli()方法完成扮演者的注入工作。
代碼清單3-8 Director:通過接口方法注入革離扮演者
- public class Director {
- public void direct(){
- GeLi geli = new LiuDeHua();
- MoAttack moAttack = new MoAttack();
- moAttack. injectGeli (geli);
- moAttack.cityGateAsk();
- }
- }
由於通過接口注入需要額外聲明一個接口,增加了類的數目,而且它的效果和屬性注入並無本質區別,因此我們不提倡採用這種方式。
通過容器完成依賴關係的注入
雖然MoAttack和LiuDeHua實現瞭解耦,MoAttack無須關注角色實現類的實例化工作,但這些工作在代碼中依然存在,只是轉移到Director類中而已。假設某一製片人想改變這一局面,在選擇某個劇本後,希望通過一個“海選”或者第三中介機構來選擇導演、演員,讓他們各司其職,那劇本、導演、演員就都實現解耦了。
所謂媒體“海選”和第三方中介機構在程序領域即是一個第三方的容器,它幫助完成類的初始化與裝配工作,讓開發者從這些底層實現類的實例化、依賴關係裝配等工作中脫離出來,專注於更有意義的業務邏輯開發工作。這無疑是一件令人嚮往的事情,Spring就是這樣的一個容器,它通過配置文件或註解描述類和類之間的依賴關係,自動完成類的初始化和依賴注入的工作。下面是Spring配置文件的對以上實例進行配置的配置文件片斷:
- <?xml version="1.0" encoding="UTF-8" ?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:p="http://www.springframework.org/schema/p"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
- <!--①實現類實例化-->
- <bean id="geli" class="LiuDeHua"/>
- <bean id="moAttack" class="com.baobaotao.ioc.MoAttack"
- p:geli-ref="geli"/><!--②通過geli-ref建立依賴關係-->
- </beans>
通過new XmlBeanFactory(“beans.xml”)等方式即可啓動容器。在容器啓動時,Spring根據配置文件的描述信息,自動實例化Bean並完成依賴關係的裝配,從容器中即可返回準備就緒的Bean實例,後續可直接使用之。
Spring爲什麼會有這種“神奇”的力量,僅憑一個簡單的配置文件,就能魔法般地實例化並裝配好程序所用的Bean呢?這種“神奇”的力量歸功於Java語言本身的類反射功能。