摘要:本文对IoC模式、依赖注入(Dependency
Injection) 模式做了简要介绍,文中分析构造子注入模式与其他模式相比较的优势和特点,并给出了在JAVA中实现该模式的方法。
1 引言
IoC(Inversion of Control)模式以被目前的轻量级容器所广泛应用,通过IoC模式这些容器帮助开发者将来自不同项目的组件装配成一个内聚的应用程序。轻量级的IoC容器(如Spring、pico-container)虽然为我们的开发提供了很大的便利,但是在很多情况下这些轻量级容器所提供的功能并不一定非常适合我们的需要,也许这些容器的功能过于庞大了,或者所提供的功能缺乏对特定应用的针对性,或者我们需要更高的运行效率,这时我们可以在了解IoC的原理的基础上利用JAVA的反射机制自己实现灵活的、可扩展的组件机制。
2 IoC与依赖注入(Dependency Injection)模式简介
与GoF的设计模式相同,IoC模式同样是关注重用性,但与GoF模式不同的是IoC模式更加关注二进制级的重用性和可扩展性,即可以直接通过二进制级进行扩充,复用的模块通常被称为组件或者插件,组件和插件都是在运行时进行装载的。
GoF的设计模式中我们大量看到的是面向接口编程:Interface Driven Design 接口驱动,接口驱动有很多好处,可以提供不同灵活的子类实现,增加代码稳定和健壮性等等,但是接口一定是需要实现的,也就是如下语句迟早要执行:
AInterface a = new AInterfaceImp();
由于以上的代码被写入了调用者程序中,同时象AinterfaceImp这样的接口的实现类是在编译时被装载的,如果以后想加入新的接口实现类则必须修改调用者的代码。
IoC模式与以上情况不同,接口的实现类是在运行时被装载的,这样即使以后新添加了接口实现类是也不需修改调用者的代码(可以通过特定的方式来定位新增的实现类,如配置文件指定)。IoC英文为
Inversion of Control,即反转模式,这里有著名的好莱坞理论:你呆着别动,到时我会找你。
IoC模式可以延缓接口的实现,根据需要实现,有个比喻:接口如同空的模型套,在必要时,需要向模型套注射石膏,这样才能成为一个模型实体,因此,对于这些新生的容器,它们反转的是“如何定位插件的具体实现”。因此,
Martin Fowler 给这种模式起了一个形象的名称“依赖注入”(Dependency Injection)。
图表 1 采用Dependency Injection前后的依赖关系变化
依赖注入的形式主要有三种,分别将它们叫做构造子注入(Constructor Injection)、设值方法注入(Setter
Injection)和接口注入(Interface Injection)。
这三种方式在Martin Fowler的《Inversion of Control Containers
and the Dependency Injection pattern》中都给出了详细的定义及说明,本文就不再赘述了,下面的内容将着重介绍构造子注入模式的特点及实现方法。
3 构造子注入模式的特点及实现
3.1 构造子注入模式的特点
通常情况下设值方法注入和接口注入较易于被开发人员接受,而构造子注入则应用较少,实际上构造子注入具有很多其他两者所不具有的优势:
1 构造子注入形成了一种更强的依赖契约
2 可以获得更加简明的代码
3 更加简明的依赖声明机制,无须定义XML配置文件或设值方法
4 更加符合接口与实现分离的组件特征,组件接口表明能够向其它组件提供的服务,而实现则应该是所提供服务的实现应该与服务契约无关(即不应包含用于获得依赖的设值方法等)。
5 不会出现不确定的状态。在设值方法注入中,由于并不是所有的设值方法(setter)都一定会被调用的,所以会有不确定状态。
从以上几点我们还可以分析出构造子注入对于组件代码的入侵性远小于其它两种模式(接口注入使得组件必须实现特定接口,设值方法同样要求组件提供特定的setter方法),代码更加易于维护。
图表 2 示例中类的关系
Client的实现依赖于接口A、B和C的实现,但是为了提供系统更好的灵活性和可扩展性,各接口的实现以组件的方式利用java的反射机制进行运行时装载,注意到组件间可能会存在某种依赖关系,例如组件AX依赖与接口B的实现类,而这中依赖关系必须在运行时动态注入,组件为了告诉组件的调用者这种依赖关系以便注入,可以使用上文提到的各种模式:
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 使用设值注入模式
public class AImp implements A {
…
public void setB(B bImp);
public void setC(C cImp);
…
}
3 使用构造子注入模式
public class AImp implements A {
…
public AImp(B bImp, C cImp){
…
}
…
}
由以上实例可以清楚的看出采用构造子注入模式的实现组件代码最为简单,且所受的入侵性最小。
3.2 在JAVA中实现构造子注入模式
在java及.NET这样具有反射功能的语言中实现类型的运行时载入并不复杂,只要通过Class.forName或生成自己的ClassLoader就可以实现。
同样我们可以通过反射机制获取组件构造函数的参数,注入相应接口的实现,作者将此过程进行了封装,以下是代码:
public class
RefectHelper
{
public
Object
ConstructorHelper(String
className,ConstructorParamDeal
pd) throws
Exception{
try{
//获取类中的构造函数
Constructor[]
constructs=Class.forName(className).getConstructors();
//实现中默认使用第一个构造函数类创建实例
Class []
classes=constructs[0].getParameterTypes();
//获取要注入的参数实例
Object []obj=pd.dealParam(classes);
//创建实例
return
constructs[0].newInstance(obj);
}catch(Exception
e){
throw e;
}
}
}
/**
*构造函数参数注入
**/
public
interface
ConstructorParamDeal
{
/**
*根据构造函数中参数的类型注入,相应的实现
@param
classes
构造函数的参数类型
◎return
注入构造函数的参数实现
**/
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++){
//为不同类型注入选择不同实例
if (classes[i].equals(String.class)){
obj[i]=”Hello
World”;
}
}
return obj;
}
}
上面的程序中ConstructorHelper用于利用反射机制枚举出载入类的构造函数及构造函数的参数的类型,至于不同类型注入什么样的实例则由ContructorParamDeal的实现者来决定,ContructorParamDeal的实现者同样可以以组件的形式在运行时动态载入。由于组件间的依赖关系的制约,所以组件实例化的顺序需要特别考虑。
4 结束语
三种依赖注入模式各有其特点和优势,只有充分理解这些模式间的不同,才能为自己的应用选择正确的依赖注入模式,文中介绍的构造子注入模式实现方法,在使用其他具有反射功能的语言(如:.NET)时同样可以参考。
|