之前對依賴註入的概念一直感到模糊,直到看了這篇文章:http://www.iteye.com/topic/1122835 引述: IoC(控制反轉:Inverse of Control)是Spring容器的內核,AOP、聲明式事務等功能在此基礎上開花結果。但是IoC這個重要的概念卻比較晦澀隱諱,不容 ...
之前對依賴註入的概念一直感到模糊,直到看了這篇文章:http://www.iteye.com/topic/1122835
引述:
IoC(控制反轉:Inverse of Control)是Spring容器的內核,AOP、聲明式事務等功能在此基礎上開花結果。但是IoC這個重要的概念卻比較晦澀隱諱,不容易讓人望文生義,這不能不說是一大遺憾。不過IoC確實包括很多內涵,它涉及代碼解耦、設計模式、代碼優化等問題的考量,我們打算通過一個小例子來說明這個概念。
通過實例理解IoC的概念
賀歲大片在中國已經形成了一個傳統,每到年底總有多部賀歲大片紛至沓來讓人應接不暇。在所有賀歲大片中,張之亮的《墨攻》算是比較出彩的一部。該片講述了戰國時期墨家人革離幫助梁國反抗趙國侵略的個人英雄主義故事,恢巨集壯闊、渾雄凝重的歷史場面相當震撼。其中有一個場景:當劉德華所飾演的墨者革離到達梁國都城下,城上梁國守軍問到:“來者何人?”劉德華回答:“墨者革離!”我們不妨通過一個Java類為這個“城門叩問”的場景進行編劇,並藉此理解IoC的概念:
MoAttack:通過演員安排劇本 :
public class MoAttack { public void cityGateAsk(){ //①演員直接侵入劇本 LiuDeHua ldh = new LiuDeHua(); ldh.responseAsk("墨者革離!"); } }
我們會發現以上劇本在①處,作為具體角色飾演者的劉德華直接侵入到劇本中,使劇本和演員直接耦合在一起(圖3-1)。
一個明智的編劇在劇情創作時應圍繞故事的角色進行,而不應考慮角色的具體飾演者,這樣才可能在劇本投拍時自由地遴選任何適合的演員,而非綁定在劉德華一人身上。通過以上的分析,我們知道需要為該劇本主人公革離定義一個介面:
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所示:
MoAttack:通過構造函數註入革離扮演者
public class MoAttack { private GeLi geli; //①註入革離的具體扮演者 public MoAttack(GeLi geli){ this.geli = geli; } public void cityGateAsk(){ geli.responseAsk("墨者革離!"); } }
MoAttack的構造函數不關心具體是誰扮演革離這個角色,只要在①處傳入的扮演者按劇本要求完成相應的表演即可。角色的具體扮演者由導演來安排,如代碼清單3-4所示:
Director:通過構造函數註入革離扮演者
public class Director { public void direct(){ //①指定角色的扮演者 GeLi geli = new LiuDeHua(); //②註入具體扮演者到劇本中 MoAttack moAttack = new MoAttack(geli); moAttack.cityGateAsk(); } }
在①處,導演安排劉德華飾演革離的角色,併在②處,將劉德華“註入”到墨攻的劇本中,然後開始“城門叩問”劇情的演出工作。
屬性註入
有時,導演會發現,雖然革離是影片《墨攻》的第一主角,但並非每個場景都需要革離的出現,在這種情況下通過構造函數註入相當於《墨攻》劇組每時每刻都要革離的飾演者在場,可見並不符合實際,這時可以考慮使用屬性註入。屬性註入可以有選擇地通過Setter方法完成調用類所需依賴的註入,意為著《墨攻》劇組可在橋段需要時讓革離的飾演者在場:
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的具體扮演者。
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介面提供具體的實現:
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()方法完成扮演者的註入工作。
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語言本身的類反射功能。