依賴注入(Dependency Injection)模式的特點(diǎn)分析與實(shí)現(xiàn)
――構(gòu)造子注入(Constructor Injection)模式的分析與實(shí)現(xiàn)
蔡 超
(chaocai2001@yahoo.com.cn)
摘要:本文對(duì)IoC模式、依賴注入(Dependency Injection) 模式做了簡(jiǎn)要介紹,文中分析構(gòu)造子注入模式與其他模式相比較的優(yōu)勢(shì)和特點(diǎn),并給出了在JAVA中實(shí)現(xiàn)該模式的方法。
1 引言
IoC(Inversion of Control)模式以被目前的輕量級(jí)容器所廣泛應(yīng)用,通過(guò)IoC模式這些容器幫助開(kāi)發(fā)者將來(lái)自不同項(xiàng)目的組件裝配成一個(gè)內(nèi)聚的應(yīng)用程序。輕量級(jí)的IoC容器(如Spring、pico-container)雖然為我們的開(kāi)發(fā)提供了很大的便利,但是在很多情況下這些輕量級(jí)容器所提供的功能并不一定非常適合我們的需要,也許這些容器的功能過(guò)于龐大了,或者所提供的功能缺乏對(duì)特定應(yīng)用的針對(duì)性,或者我們需要更高的運(yùn)行效率,這時(shí)我們可以在了解IoC的原理的基礎(chǔ)上利用JAVA的反射機(jī)制自己實(shí)現(xiàn)靈活的、可擴(kuò)展的組件機(jī)制。
2 IoC與依賴注入(Dependency Injection)模式簡(jiǎn)介
與GoF的設(shè)計(jì)模式相同,IoC模式同樣是關(guān)注重用性,但與GoF模式不同的是IoC模式更加關(guān)注二進(jìn)制級(jí)的重用性和可擴(kuò)展性,即可以直接通過(guò)二進(jìn)制級(jí)進(jìn)行擴(kuò)充,復(fù)用的模塊通常被稱(chēng)為組件或者插件,組件和插件都是在運(yùn)行時(shí)進(jìn)行裝載的。
GoF的設(shè)計(jì)模式中我們大量看到的是面向接口編程:Interface Driven Design 接口驅(qū)動(dòng),接口驅(qū)動(dòng)有很多好處,可以提供不同靈活的子類(lèi)實(shí)現(xiàn),增加代碼穩(wěn)定和健壯性等等,但是接口一定是需要實(shí)現(xiàn)的,也就是如下語(yǔ)句遲早要執(zhí)行:
AInterface a = new AInterfaceImp();
由于以上的代碼被寫(xiě)入了調(diào)用者程序中,同時(shí)象AinterfaceImp這樣的接口的實(shí)現(xiàn)類(lèi)是在編譯時(shí)被裝載的,如果以后想加入新的接口實(shí)現(xiàn)類(lèi)則必須修改調(diào)用者的代碼。
IoC模式與以上情況不同,接口的實(shí)現(xiàn)類(lèi)是在運(yùn)行時(shí)被裝載的,這樣即使以后新添加了接口實(shí)現(xiàn)類(lèi)是也不需修改調(diào)用者的代碼(可以通過(guò)特定的方式來(lái)定位新增的實(shí)現(xiàn)類(lèi),如配置文件指定)。IoC英文為 Inversion of Control,即反轉(zhuǎn)模式,這里有著名的好萊塢理論:你呆著別動(dòng),到時(shí)我會(huì)找你。
IoC模式可以延緩接口的實(shí)現(xiàn),根據(jù)需要實(shí)現(xiàn),有個(gè)比喻:接口如同空的模型套,在必要時(shí),需要向模型套注射石膏,這樣才能成為一個(gè)模型實(shí)體,因此,對(duì)于這些新生的容器,它們反轉(zhuǎn)的是“如何定位插件的具體實(shí)現(xiàn)”。因此, Martin Fowler 給這種模式起了一個(gè)形象的名稱(chēng)“依賴注入”(Dependency Injection)。
圖表 1 采用Dependency Injection前后的依賴關(guān)系變化
依賴注入的形式主要有三種,分別將它們叫做構(gòu)造子注入(Constructor Injection)、設(shè)值方法注入(Setter Injection)和接口注入(Interface Injection)。
這三種方式在Martin Fowler的《Inversion of Control Containers and the Dependency Injection pattern》中都給出了詳細(xì)的定義及說(shuō)明,本文就不再贅述了,下面的內(nèi)容將著重介紹構(gòu)造子注入模式的特點(diǎn)及實(shí)現(xiàn)方法。
3 構(gòu)造子注入模式的特點(diǎn)及實(shí)現(xiàn)
3.1 構(gòu)造子注入模式的特點(diǎn)
通常情況下設(shè)值方法注入和接口注入較易于被開(kāi)發(fā)人員接受,而構(gòu)造子注入則應(yīng)用較少,實(shí)際上構(gòu)造子注入具有很多其他兩者所不具有的優(yōu)勢(shì):
1 構(gòu)造子注入形成了一種更強(qiáng)的依賴契約
2 可以獲得更加簡(jiǎn)明的代碼
3 更加簡(jiǎn)明的依賴聲明機(jī)制,無(wú)須定義XML配置文件或設(shè)值方法
4 更加符合接口與實(shí)現(xiàn)分離的組件特征,組件接口表明能夠向其它組件提供的服務(wù),而實(shí)現(xiàn)則應(yīng)該是所提供服務(wù)的實(shí)現(xiàn)應(yīng)該與服務(wù)契約無(wú)關(guān)(即不應(yīng)包含用于獲得依賴的設(shè)值方法等)。
5 不會(huì)出現(xiàn)不確定的狀態(tài)。在設(shè)值方法注入中,由于并不是所有的設(shè)值方法(setter)都一定會(huì)被調(diào)用的,所以會(huì)有不確定狀態(tài)。
從以上幾點(diǎn)我們還可以分析出構(gòu)造子注入對(duì)于組件代碼的入侵性遠(yuǎn)小于其它兩種模式(接口注入使得組件必須實(shí)現(xiàn)特定接口,設(shè)值方法同樣要求組件提供特定的setter方法),代碼更加易于維護(hù)。
圖表 2 示例中類(lèi)的關(guān)系
Client的實(shí)現(xiàn)依賴于接口A、B和C的實(shí)現(xiàn),但是為了提供系統(tǒng)更好的靈活性和可擴(kuò)展性,各接口的實(shí)現(xiàn)以組件的方式利用java的反射機(jī)制進(jìn)行運(yùn)行時(shí)裝載,注意到組件間可能會(huì)存在某種依賴關(guān)系,例如組件AX依賴與接口B的實(shí)現(xiàn)類(lèi),而這中依賴關(guān)系必須在運(yùn)行時(shí)動(dòng)態(tài)注入,組件為了告訴組件的調(diào)用者這種依賴關(guān)系以便注入,可以使用上文提到的各種模式:
1 使用接口注入模式
public interface InjectB{
public void injectB(B bImp);
}
public interface InjectC{
public void injectC(C cImp);
}
public class AImp implements A,InjectB,InjectC{
…
public void injectB(B bImp);
public void injectC(C cImp);
…
}
2 使用設(shè)值注入模式
public class AImp implements A {
…
public void setB(B bImp);
public void setC(C cImp);
…
}
3 使用構(gòu)造子注入模式
public class AImp implements A {
…
public AImp(B bImp, C cImp){
…
}
…
}
由以上實(shí)例可以清楚的看出采用構(gòu)造子注入模式的實(shí)現(xiàn)組件代碼最為簡(jiǎn)單,且所受的入侵性最小。
3.2 在JAVA中實(shí)現(xiàn)構(gòu)造子注入模式
在java及.NET這樣具有反射功能的語(yǔ)言中實(shí)現(xiàn)類(lèi)型的運(yùn)行時(shí)載入并不復(fù)雜,只要通過(guò)Class.forName或生成自己的ClassLoader就可以實(shí)現(xiàn)。
同樣我們可以通過(guò)反射機(jī)制獲取組件構(gòu)造函數(shù)的參數(shù),注入相應(yīng)接口的實(shí)現(xiàn),作者將此過(guò)程進(jìn)行了封裝,以下是代碼:
public class RefectHelper {
public Object ConstructorHelper(String className,ConstructorParamDeal pd) throws Exception{
try{
//獲取類(lèi)中的構(gòu)造函數(shù)
Constructor[] constructs=Class.forName(className).getConstructors(); //實(shí)現(xiàn)中默認(rèn)使用第一個(gè)構(gòu)造函數(shù)類(lèi)創(chuàng)建實(shí)例
Class [] classes=constructs[0].getParameterTypes();
//獲取要注入的參數(shù)實(shí)例
Object []obj=pd.dealParam(classes);
//創(chuàng)建實(shí)例
return constructs[0].newInstance(obj);
}catch(Exception e){
throw e;
}
}
}
/**
*構(gòu)造函數(shù)參數(shù)注入
**/
public interface ConstructorParamDeal {
/**
*根據(jù)構(gòu)造函數(shù)中參數(shù)的類(lèi)型注入,相應(yīng)的實(shí)現(xiàn)
@param classes 構(gòu)造函數(shù)的參數(shù)類(lèi)型
◎return 注入構(gòu)造函數(shù)的參數(shù)實(shí)現(xiàn)
**/
public Object [] dealParam(Class [] classes);
}
public class ParamDeal implements ConstructorParamDeal{
/* (non-Javadoc)
* @see com.topsec.tsm.agent.helper.ConstructorParamDeal#dealParam(java.lang.Class[])
*/
public Object [] dealParam(Class[] classes) {
Object [] obj=new Object[classes.length];
for (int i=0;i<obj.length;i++){
//為不同類(lèi)型注入選擇不同實(shí)例
if (classes[i].equals(String.class)){
obj[i]=”Hello World”;
}
}
return obj;
}
}
上面的程序中ConstructorHelper用于利用反射機(jī)制枚舉出載入類(lèi)的構(gòu)造函數(shù)及構(gòu)造函數(shù)的參數(shù)的類(lèi)型,至于不同類(lèi)型注入什么樣的實(shí)例則由ContructorParamDeal的實(shí)現(xiàn)者來(lái)決定,ContructorParamDeal的實(shí)現(xiàn)者同樣可以以組件的形式在運(yùn)行時(shí)動(dòng)態(tài)載入。由于組件間的依賴關(guān)系的制約,所以組件實(shí)例化的順序需要特別考慮。
4 結(jié)束語(yǔ)
三種依賴注入模式各有其特點(diǎn)和優(yōu)勢(shì),只有充分理解這些模式間的不同,才能為自己的應(yīng)用選擇正確的依賴注入模式,文中介紹的構(gòu)造子注入模式實(shí)現(xiàn)方法,在使用其他具有反射功能的語(yǔ)言(如:.NET)時(shí)同樣可以參考。
[參考文獻(xiàn)]
1 Martin Fowler,Inversion of Control Containers and the Dependency Injection pattern,http://www.martinfowler.com/articles/injection.html,2004
2 Erich Gamma,Design Patterns,Addison Wesley,1999
3 http://www.picocontainer.org/
4 彭晨陽(yáng),Ioc模式, http://www.jdon.com,2004