1引言
面向对象的思想已经深入人心,但是要利用面向对象的思想开发出优秀的应用程序却不是一件容易的事情。正是基于面向对象的思想,人们对各种应用程序进行了大量的分析、总结、归纳出了设计模式。Alexanders给出模式的经典定义是:每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心。通过这种方式,你可以无数次地使用那些已有的解决方案,无需再重复相同的工作[2]。设计模式技术在GoF的经典书籍面世以来,得到了广泛的关注、研究与应用。时下如雨后春笋般涌现的各种框架都是利用设计模式的典范。其中IoC模式更是广泛应用于各种框架中。
2 IoC模式
2.1 IoC模式简介
IoC(Inversion of Control)模式并不是什么新的东西,它是一种很普遍的概念,GoF中的Template Method 就是IoC的结构。顾名思义,IoC即控制反转。著名的好莱坞原则:“Don’t Call us, We will call you”,以及Robert C. Martin在其敏捷软件开发中所描述的依赖倒置原则(Dependency Inversion Principle, DIP)都是这一思想的体现。依赖注入(Dependency Injection)是Martin Flower对IoC模式的一种扩展的解释[2]。IoC是一种用来解决组件(实际上也可以是简单的Java类)之间依赖关系、配置及生命周期的设计模式,其中对组件依赖关系的处理是IoC的精华部分。IoC的实际意义就是把组件之间的依赖关系提取(反转)出来,由容器来具体配置。这样,各个组件之间就不存在hard-code的关联,任何组件都可以最大程度的得到重用。运用了IoC模式后我们不再需要自己管理组件之间的依赖关系,只需要声明由容器去实现这种依赖关系。就好像把对组件之间依赖关系的控制进行了倒置,不再由组件自己来建立这种依赖关系而交给容器(例如我们后面会介绍的PicoContainer、Spring)去管理。
我们从一个简单的例子看起,考虑一个Button控制Lamp的例子:
public class Button {
private Lamp lamp;
public void push() {
lamp.turnOn();
}
}
但是马上发现这个设计的问题,Button类直接依赖于Lamp类,这个依赖关系意味着当Lamp类修改时,Button类会受到影响。此外,想重用Button类来控制类似与Lamp的(比如同样具有turnOn功能的Computer)另外一个对象则是不可能的。即Button控制Lamp,并且只能控制Lamp。显然违反了“高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,细节应该依赖于抽象” 这一原则(DIP原则)。考虑到上述问题,自然的想到应该抽象出一个接口SwitchableDevice,来消除Button对Lamp的依赖,于是设计如下:
public class Button {
private SwitchableDevice lamp;
public Button(){
lamp= new Lamp();
}
}
再深入考虑一下,虽然我们的Button现在可以控制实现了SwitchableDevice接口的Computer,但是Button和Lamp类之间还是存在create这样的依赖关系。为了解决这种依赖关心,经典的GoF模式就是采用Factory模式,将对象的创建交给Factory类来创建,但是这种创建仍是显示的,组件变化了仍然需要重新编译程序。而采用J2EE经典的service locator模式,如果你要把Button组件拿到另一个系统里面用,你就必须修改它的源码,让它使用另一个系统的serviceLocator。换句话说,这个组件不具备可移植性。这就是需要依赖注入的道理,让组件的创建、配置及生命周期总是由外部容器来管理。
2.2 IoC的类型
2.2.1 IoC的类型
在介绍如何利用IoC模式实现彻底解耦之前,我们先看看IoC的类型:
2.2.1.1 Method-based (M) IoC
在每个方法调用中传递其依赖的组件。如果方法需要某个组件,就把该组件作为参数传递给方法。
2.2.1.2 Interface-based (I) IoC (Type 1)
使用接口如Serviceable, Configurable 等等,来声明依赖。EJB容器就是一个Type1的重量级容器,部署在它内部的EJB组件使用接口来声明依赖关系。
2.2.1.3 Setter-based (S) IoC (Type 2)
使用setters 来设置依赖组件。把依赖的组件作为一个属性,通过setters方法来动态设置依赖组件。
2.2.1.4 Constructor-based (C) IoC (Type 3)
使用构造函数来声明依赖。通过传递组件参数到构造函数中,来实现依赖关系。
2.2.2 IoC类型的比较
这几种类型中,type 3侵入性较小。因为在面向对象的理论里,constructor并不是对象契约的一部分。按照Bertrand Meyer的说法,你永远不应该直接调用constructor,因为这就意味着client代码与实现(而非契约)绑定在一起。那么,既然constructor并不属于对象契约的一部分,在constructor里暴露元信息就不会影响对象契约。Type 2虽然也很好,但setter毕竟属于对象契约,把一个setter用于IoC多少有一点“破坏性”,而且通过setter方法过多的暴露了内部对象的内部细节,这就失去了对象的封装。
Type 2很合适的作为应用程序的bean工厂。如果是更多的动态组装,可能type 3更好一点。从定义上来说,type 2是基于setter的,type 3是基于constructor的。为什么说type 2更适合于做bean工厂呢?因为setter是各个分离的,对于有定义的n个setter,bean工厂调用其中的0~n个都是合法的。而type 3则稍微有点麻烦,不能适应依赖较多的情况,组件的“元信息”在constructor的参数列表中体现,你必须一次性提供所有必要的参数。如果需要很多组件,就需要在构造函数中传递很多参数,这样会导致constructor的参数过多过长。
2.3 IoC容器
根据容器对组件的侵入的程度,可以把IoC容器分为以下三类:
2.3.1 Interface Injection
对应Type 1 IoC ,使用接口来声明依赖。这类IoC容器侵入性最强,需要通过上下文来获取组件.组件需要实现容器提供的特定接口,这样,组件的重用就被限定在该容器内。这类容器的代表有Apache Avalon。Avalon 不怎么流行,尽管它很强大而且有很长的历史。Avalon属于重量级容器,并且看起来比新的IoC解决方案更具侵入性。
2.3.2 Setter Injection
对应Type 2 IoC ,使用setters来设置依赖组件。这类IoC容器需要组件提供accessor方法,依赖关系通过setter方法来注入。按照java组件模型,一般的javabean都会有accessor方法,因此组件的重用性没有任何限制。这类容器的代表有Spring,同时它也实现了第三类IoC容器。Spring是一个非常活跃的、优秀的开源项目。它是一个基于IoC和AOP(Aspect-Oriented Programming,面向方面编程)的构架多层J2EE系统的框架,它优雅的实现了MVC框架,支持使用可声明事务管理(declarative transaction management)。更重要的是Spring框架的无侵入性[3]。
2.3.3 Constructor Injection
对应Type 3 IoC ,使用构造函数来声明依赖。这类IoC容器需要组件由构造方法来配置依赖关系。和第二种IoC类型类似,组件重用没有任何问题。并且Constructor Injection更加严格,完全按照契约(contract)来配置组件依赖。这类容器的代表有PicoContainer。
PicoContainer是一个轻量级而且更强调通过构造函数表达依赖性,而不是JavaBean 属性。 与Spring不同,它的设计允许每个类型一个对象的定义(可能是因为它拒绝任何Java代码外的元数据导致的局限性)。
2.4 利用IoC容器实现控制反转
下面我们就来看看如何利用IoC容器PicoContainer实现本文开始处举的例子,主要代码如下:
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
pico.registerComponentImplementation(SwitchableDevice.class, Lamp.class);
pico.registerComponentImplementation(Button.class);
return pico;
}
然后就可以通过MutablePicoContainer的getComponentImplementation方法获得实现类,调用其push方法控制Lamp的开关,这样一来,两者之间的耦合通过PicoContainer提供的Assembler完全消除了。
Spring则通过一个XML格式的配置文件,将两者联系起来。使用时,通过ApplicationContext获得Button bean,再调用其方法实现,同样也消除了耦合关系。
3总结
IoC具有以下几个优点:
1.因为组件不需要在运行时寻找合作者,所以他们可以更简单的编写和维护。由于同 样原因,便于编写测试代码,使类的测试更容易。
2.不需要外部依赖。能在任何环境下开发和测试组件,而不需要特殊的部署环境,像 JNDI、EJB那样。并且在不同IoC容器中可方便的重用和改变。
3.整个系统更容易组装和配置。大部分业务对象不依赖于IoC容器的APIs。这使得很 容易使用遗留下来的代码,且很容易的使用对象,无论在容器内或不在容器内。
4.增加组件的复用程度,提供软件生成效率。
当然,IoC与通常的方法相比,代码不便于理解,因为组件创建是隐含的。所以轻量级的、无侵入性的IoC容器仍然有待我们去研究开发。