本文的作者Danny hui似乎是TTS上的新人,我从Google搜不出一点关于本人的信息。从通过本文可以看出他对模式与IoC有自己独到的见解,本文在TTS上引发很多网友回帖,反响不一。那么我们现在来看看作者的IoC之路吧。
原文:http://www.theserverside.com/tt/articles/article.tss?l=InjectionwithAbstractFactory
简介
本文重点讨论的是DI(依赖注入)结合设计模式中的Abstract Factory(抽象工厂模式)的优势与弊端。该方式尤其适合以下场合:
l 通过dynamic parameters(动态参数)来建立一个local stateful对象
l 当创建对象时抛出了checked exception时进行相应处理,
l 动态的封装对象
像Spring IoC容器,PicoContainer以及Guice都无法圆满的解决这些问题或者说它们几乎做不到!!!
用Abstractory Factory模式来实现DI
现在通过下面两种途径来改进经典GoF中的Abstract Factory:
l 一个工厂接口来代替抽象工厂类(可行)
l 每一个工厂方法的职责就是创建对象,并为其注入依赖
来看一个简单的例子吧:
在这里,ComponentA依赖于ComponentB。为方便进行单元测试ComponentAImpl类,接口ComponentB的实现必须注入到ComponentAImpl中去。下面看看采用Abstract Factory模式完成依赖注入的Java实现代码:
//Abstract Factory for Dependency Injection
//Factory interface
public interface Module1ServiceFactory {
ComponentA getComponentA();
ComponentB getComponentB();
}
//Concrete factory
public class Module1ServiceFactoryImpl implements Module1ServiceFactory {
private ComponentA componentA;
private ComponentB componentB;
private Module1Servicefactory instance;
private Module1ServicefactoryImpl() {}
public static synchronized Module1ServiceFactory getInstance() {
if (null == instance) {
instance = new Module1ServicefactoryImpl();
componentA = new ComponentAImpl();
componentB = new ComponentBImpl();
componentA.setComponentB(componentB);
}
return instance;
}
public ComponentA getComponentA() {
return componentA;
}
public ComponentB getComponentB() {
return componentB;
}
}
//Client
public class Client {
public static void main(String[] args) {
Module1ServiceFactory m1sf =
Module1ServicefactoryImpl.getInstance();
ComponentA componentA = m1sf.getComponentA();
componentA.operationA1();
}
}
延迟加载
延迟加载对象可以通过改变方法来实现,比如说我现在要稍微对getComponentA()进行改动,请看:
public synchronized ComponentA getComponentA() {
if (null == componentA) {
componentA = new ComponentAImpl();
}
return componentA;
}
当然我们的Module1ServiceFactoryImpl.getInstance()方法也要进行相应的改动了,我们可以通过传递一个参数来判断Module1ServiceFactoryImpl.getInstance()是否需要创建对象。
非Singleton作用域
上面的代码仅仅只是建立singleton对象。如果需要在每次调用getComponentA()和getComponentB()的时,都返回新创建的对象的话,我们可以对我们的Abstract Factory进行下面的改动:
//Concrete factory
public class Module1ServiceFactoryImpl {
private Module1ServiceFactory instance;
private Module1ServiceFactoryImpl() {}
public static synchronized Module1ServiceFactory getInstance() {
if (null == instance) {
instance = new Module1ServiceFactoryImpl();
}
return instance;
}
public ComponentA getComponentA() {
ComponentA componentA = new ComponentAImpl();
ComponentB componentB = getComponentB();
componentA.setComponentB(componentB);
return componentA;
}
public ComponentB getComponentB() {
return new ComponentBImpl();
}
}
类似的,我们还可以将一个singleton对象注入到非singleton对象中去。比如说,我们假设ComponentB此时是singleton,ComponentA为非singleton,那么我们可以这样:
//Concrete factory
public class Module1ServiceFactoryImpl {
private Module1ServiceFactory instance;
private ComponentB componentB;
private Module1ServicefactoryImpl() {}
public static synchronized Module1ServiceFactory getInstance() {
if (null == instance) {
instance = new Module1ServiceFactoryImpl();
componentB = new ComponentBImpl();
}
return instance;
}
public ComponentA getComponentA() {
ComponentA componentA = new ComponentAImpl();
componentA.setComponentB(componentB);
return componentA;
}
public ComponentB getComponentB() {
return componentB;
}
}
将一个非singleton对象注入到singleton对象也不是做不到,但这种应用场合在现实世界中是非常罕见的。尽管如此,但在使用dynamic parameters赋级一个locallocal变量时,创建一个非singleton对象却很普遍。接下的话题,我们就谈谈这个。
用dynamic parameters为singleton创建一个local stateful对象
这是所有IoC框架所面临的问题。下面的代码中,仍然假定ComponentA为singletion:
public void operationA2() {
String s = aPrivateMethod();
int i = anotherMethod();
ComponentC componentC = new ComponentCImpl(s, i);
//do something else.
}
这里,ComponentAImpl用到了ComponentC接口。虽然,为了更IoC,我们需要将它注入ComponentC的实现,而不是直接硬编码在ComponentAImpl.operationA2()方法中去,这样做还有一个好处就是,方便单元测试。但问题来了,我们不能将ComponentC作为一个实例变量,因为它是有状态的(stateful),它维持着某一个特定的客户端状态,不能与其它客户端进行共享。因此,不能使用常用的setter或construtctor注入方法来实现。
使用Abstract Factory模式的话,有两个方法可以解决这个问题。不过,都得改动Module1ServiceFactory接口,添加下面方法:
ComponentC getComponentC(String s, int i);
请看我在Module1ServiceFactoryImpl中的实现代码:
public ComponentC getComponentC(String s, int i) {
return new ComponentCImpl(s, i);
}
第一种方法就是将包含它的“工厂”注入到所需的local stateful对象中去:
private Module1ServiceFactory factory;
public void setModule1ServiceFactory(Module1ServiceFactory factory) {
this.factory = factory;
}
//ComponentAImpl.operationA2() becomes:
public void operationA2() {
String s = aPrivateMethod();
int i = anotherMethod();
ComponentC componentC = factory.getComponentC(s, i);
//do something else.
}
缺点显而易见:ComponentAImpl现在与Module1ServiceFactory绑定在一起了,如果要对ComponentAImpl进行单元测试的话,我们不得不mock一个Module1ServiceFactory实现。纵然有这个缺陷,但直接为stateful对象注入“工厂”对象的方法也最为简单。类似的技术广泛的在J2EE领域采用,比如说JPA,将我们将entity manager factory可以注入到应用程序代码后,它就专门负责管理自身创建的application-managed entity。(注:如果熟悉Hibernate的话,也可以将entity manager factory想象成HibernateSessionFacory, application-managed entity想象成Session)
第二种方法就是将方法抽出,移到抽象类中去,便于单元测试:
public abstract class AbstractComponentA implements ComponentA {
public void operationA2() {
String s = aPrivateMethod();
int i = anotherMethod();
ComponentC componentC = getComponentC(s, i);
//do something else.
}
public abstract ComponentC getComponentC(String s, int i) ;
}
public class ComponentAImpl extends AbstractComponentA {
public ComponentC getComponentC(String s, int i) {
return new ComponentCimpl(s, i);
}
}
这种方式类似于Springframework的方法注入(Metod Injection),不过Spring不需要传递dynamic parameter也能创建stateful对象。此时,单元测试可以不需要再实现任何mock工厂。但是,这仍然是一个笨拙的办法。设想一下,我们的类里如果有10个这样的local stateful对象,那么我们需要提供10个抽象方法,才能再次让单元测试变得简单,可是代价就造成是更加混乱的应用程序代码。
Springframework还可以通过使用Java反射机构还解决类似问题。但这更加复杂了,并且不适合正常应用程序编码工作。
处理创建对象时抛出的checked exceptions
这个问题也是让IoC容器头痛的。如果checked exception在对象创建时抛出,应用程序可能希望是能捕获并且能够恢复。我们来看一下这个关于Web Service的需求实例:当客户端尝试建立web service的stub时,并且此时服务端web service还不可用,那么客户端是能够捕获stub所抛出的异常,然后显示相应信息,并询问用户是否稍后继续再次连接。如果单纯用IoC容器的话,抛出具体指定的checked exception是很困难的。而手工代码却可以很轻松的解决这个问题——我们可以简单的将“工厂”的checked exceptions抛出,留给应用程序代码去手工处理或者恢复它们。
动态封装对象
很多场合下,一个接口对应着多个不同的实现类,类型实例就是设计模式中的策略模式。那么,用一个参数就可以决定具体哪个实现类应该被注入到相应的封装对象中去。然而使用基于XML和IoC容器是静态封装对象,很难实现此功能。也许编程式的IoC容器可以解决动态依赖问题,但我要说的是我们的Abstract Factory则更简单直接,看看下面的代码就知道了:
//Concrete factory
public class Module1ServiceFactoryImpl {
...
public ComponentA getComponentA(int strategy) {
ComponentA componentA = new ComponentAImpl();
ComponentB componentB = getComponentB(strategy);
componentA.setComponentB(componentB);
return componentA;
}
public ComponentB getComponentB(int strategy) {
switch(strategy) {
case STRATEGYA:
return new StrategyA();
case STRATEGYB:
return new StrategyB();
default:
return null;
}
}
}
注意这里StrategyA和StrategyB共享实现了ComponentB接口。
结束语
今天我们谈到的运用依赖注入和Absratct Factory设计模式来解决下列问题:
1. 通过动态参数,创建local stateful对象
2. 处理创建对象时抛出的checked exceptions
3. 动态封装注入对象
除此以外,该方法与其它IoC容器相比,性能更好,毕竟是直接硬编码嘛。那么最大的缺点自然就是要手工写很多基础代码了,并且如果要延迟加载与主动加载之间来回切换的话,代码的改动量是很可观的。不过呢,这样的需求几乎是不存在的。
可单元测试的关键点是基于接口而非实体类编程。这样的话mock对象可以注入到任何需要注入的地方去。
不管怎么样,有时候在我们的应用程序中,依赖注入是一个不得不解决的问题。所有IoC容器以及手工的依赖注入解决方案都是专注于各自的领域——Spring IoC使用XML配置,Google Guice使用特殊的Java,我们的Abstract Factory也是如此。通过这些解决方案,我们可以避免应用程序中到处显现依赖的编码,类与类之间耦合度降低,使用Mock对象就可以正常进行单元测试。
对所有的IoC容器来说,无论是声明式的还是编程式的,它们的目的就是作为一个对象创建和封装工厂。同样我们的Abstract Factory也是如此,只不过这是一个可定制的依赖注入解决方案。
最后,使用IoC容器,我们可以消除类与类之间的依赖,从而让单元测试变得更加简单。但代价就是你不得不再次依赖于第三方Jar包或XML配置文件。IoC容器并没有一个统一的标准,每个框架所提供的特性和功能都是不同的,因为一旦你使用了某个IoC框架,就意味