看《墨攻》理解軟件世界的IoC概念

轉載自:http://tech.it168.com/j/e/2006-12-27/200612271430763.shtml

概述
    IoC(反向控制:Inverse of Control)是Spring容器的底層核心功能,AOP功能、聲明事務等功能在此基礎上生根開花。但是IoC這個重要的概念卻比較晦澀隱諱,拐彎抹角,不容易讓人望文生義,不能不說是一大遺憾。不過IoC確實包括很多內涵,它涉及到代碼解耦,設計模式優化等問題的考量。
2006年多部賀歲大片以讓人應接不暇的頻率紛至沓來,其中張之亮的《墨攻》算是比較出彩的一部,講述了戰國時期墨家人革離幫助梁國反抗趙國侵略的個人英雄主義故事,恢宏壯闊,渾雄凝重的歷史場面相當震撼。其中有一個場景:當劉德華所飾的墨者革離到達梁國都城下,城上樑國守軍問:“來者何人?”,劉德華回答:“墨者革離!”,我們不妨用Java對這段“城門問對”的場景進行編劇並藉由這個例子來理解IoC的內涵。

劇本和飾演者耦合
 
MoAttack代表《墨攻》的劇本,cityGetAsk()代表“城門問對”這段劇情,LiuDeHua是具體飾演者劉天王:
                                代碼清單 1  MoAttack:通過演員安排劇本

public class MoAttack { public void cityGateAsk(){ LiuDeHua ldh = new LiuDeHua(); ① 演員直接侵入劇本 ldh.responseAsk("墨者革離!"); } }

  我們會發現以上劇本在①處,作爲具體飾演者的劉德華直接侵入到劇本中,使劇本和演員直接耦合在一起:
                       

                                                           圖 1 劇本和演員直接耦合
  一個明智的編劇在劇情創作時應圍繞故事的角色進行,而不應考慮角色的具體飾演者,這樣纔可能在劇本投拍時自由地遴選任何適合的演員,而非綁定在劉德華一人身上。通過以上的分析,我們知道需要爲該劇本主人公革離定義一個接口,以角色進行劇情安排,飾演者實現角色的接口:
                代碼清單 2 MoAttack:引入劇本角色

public class MoAttack...{ public void cityGateAsk() ...{ GeLi geli = new LiuDeHua(); ① 引入革離角色接口 geli.responseAsk("墨者革離!"); ② 通過接口開展劇情 } }


    在①處引入了劇本的角色——革離,劇本的情節通過角色展開,在拍攝時角色的事蹟由演員表現,如②處所示。因此墨攻、革離、劉德華三者的類圖關係如圖 2所示:
  


    我們希望劇本和演員無關,可是,在圖2中,我們看到MoAttack同時依賴於GeLi接口和LiuDeHua類,並沒有達到我們所期望的劇本僅依賴於角色的目的。可是角色最終又必須通過具體的演員才能完成拍攝,如何將讓LiuDeHua和劇本無關而又能完成GeLi的具體動作呢?當然是在影片投拍時,導演將LiuDeHua安排在GeLi的角色上,通過導演之手將劇本、角色、飾演者裝配起來。


                                        圖 3 劇本和飾演者解耦了
    通過引入導演,劇本和具體的飾演者解耦了,對應到軟件中,導演象是一個裝配器,將具體的飾演者賦給了劇本的角色。
現在我們可以反過來講解IoC的概念了。IoC(Inverse of Control)的字面意思是控制反轉,它包括兩個層面的內容:其一是“控制”,其二是“反轉”,到底是什麼東西的控制被反轉了呢?對應到前面的例子,“控制”是指GeLi角色扮演者的選擇控制權,“反轉”是指這種選擇控制權從《墨攻》劇本中移除,轉交到導演的手中。對於程序來說,即是某一接口具體實現類的選擇控制權從客戶類中移除,轉交給第三方來確定,客戶類不知道是哪個具體的實現類,它通過接口方法對實現類進行調用。

    因爲IoC確實不夠開門見山,因此業界曾進行了廣泛的討論,最終軟件界的泰斗級人物Martin Fowler提出了DI(依賴注入:Dependency Injection)的概念,即將客戶類對接口實現類的依賴關係由第三方(容器或協作類)注入,以移除客戶類對具體接口實現類的依賴。“依賴注入”的概念顯然比“控制反轉”直接達意,易於理解。

IoC的三種類型
 
從注入方法上看,主要可以劃分爲三種的注入類型,分別是構造函數注入、屬性注入和接口注入,Spring支持構造函數注入和屬性注入。下面我們繼續使用以上的例子說明這三種注入方法的區別。

■ 構造函數注入
    我們通過客戶類的構造函數,將接口實現類通過接口變量傳入,如代碼清單 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所示:
                                            代碼清單 4 Director:通過構造函數注入革離扮演者

public class Director { public void direct(){ GeLi geli = new LiuDeHua(); ① 指定角色的扮演者 MoAttack moAttack = new MoAttack(geli); ② 注入具體扮演者到劇本中 moAttack.cityGateAsk(); } }

   在①處,導演安排劉德華飾演革離的角色,並在②處,將劉德華“注入”到墨攻的劇本中,然後開始“城門問答”劇段的演出工作。
  
■ 屬性注入
     有時,導演會發現,雖然革離是影片《墨攻》的第一主人公,但並非每場戲都需要革離的出現,通過構造函數方式注入顯得很不妥當,在這種情況下,可以使用屬性注入進行改造。屬性注入通過setter方法完成客戶類所需依賴的注入,更靈活,更方便。
                    代碼清單 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的具體扮演者,而不需要劉德華從頭到尾跟着墨攻劇組跑。
      代碼清單 6 Director:通過setter方法注入革離扮演者

public class Director { public void direct(){ GeLi geli = new LiuDeHua(); MoAttack moAttack = new MoAttack(); moAttack.setGeli(geli); ① 調用屬性setter方法注入 moAttack.cityGateAsk(); } }

   和通過構造函數注入革離扮演者不同,在實例化MoAttack時,並未指定任何扮演者,而是在實例化MoAttack後,調用其setGeli()方法注入扮演者。按照類似的方式,我們還可以爲劇本中其他如巷淹中,梁王等角色分別提供注入的setter方法,導演即可以根據所拍劇段的不同,注入所需要的角色了。

■ 接口注入
     將客戶類所有注入的方法抽取到一個接口中,客戶類通過實現這一接口提供注入的方法。爲了採取接口注入的方式,需要聲明一個額外的接口:

public interface ActorArrangable { void injectGeli(GeLi geli); }

   然後,MoAttack實現這個接口並實現接口中的方法:
             代碼清單 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()方法完成扮演者的注入工作。
   代碼清單 8 Director:通過接口方法注入革離扮演者

public class Director { public void direct(){ GeLi geli = new LiuDeHua(); MoAttack moAttack = new MoAttack(); moAttack. injectGeli (geli); moAttack.cityGateAsk(); } }

   由於通過接口注入需要額外聲明一個接口,增加了類的數目,而且它的效果和屬性注入並無本質區別,因此我們不提倡這種方式。

通過容器完成依賴關係的建立
   
雖然MoAttack和LiuDeHua實現瞭解耦,無需關注實現類的實例化工作,但這些工作在代碼中依然存在,只是轉移到Director中而已,導致導演的權力非常大,潛規則不斷滋生。假設某一製片人想改變這一局面,在相中某個劇本後,通過一個“海選”或者第三公正中介來選擇導演、演員,讓他們各司其職,那劇本、導演、演員就都實現解耦了。
所謂媒體“海選”和中介機構在程序領域即是一個第三方容器,它幫助我們完成類的初始化和裝配工作,讓我們從這些底層的實現類實例化,依賴關係的裝配中脫離出來,專注於更有意思的業務代碼的編寫工作,那確實是挺愜意的事情。Spring就是這樣一個容器,它通過配置文件描述類之間的依賴關係,下面是Spring配置文件的對以上實例進行配置的樣式代碼:

<beans> <bean id="geli" class="com.baobaotao.LiuDeHua"></bean> <bean id="moAttack" class=" com.baobaotao.MoAttack"> <property name="geli"><ref bean="geli"/></property> </bean> </beans>

    通過new XmlBeanFactory(“beans.xml”)等方式即可啓動容器,在容器啓動時,Spring根據配置文件的描述信息,通過Java的反射機制自動實例化Bean並完成依賴關係的建立,從容器中即可返回準備就緒的Bean實例,以待後續的使用。

小結
    隨着Spring的廣泛應用,IoC的概念被越來越多的提及,可是很多說者因爲IoC本身的晦澀往往並不清楚其中的意義,我們通過時下叫座的賀歲大片《墨攻》來解釋IoC的概念,相信對加深IoC概念的理解有所補益。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章