“围城”式困境中的依赖注入模式及Spring
写在前面的话
依赖注入模式、或者说IOC模式,随着Spring框架的崛起而被人老生常谈。但是,有一些人只是在使用Spring中被动的使用依赖注入模式。对于使用Spring和依赖注入模式给自己的项目带来了多少好处,我们没有仔细的计算过;我们使用Spring及依赖注入模式只是因为很多人都在这么使用,或者说轻量级容器现在很流行。
本文试图来谈谈依赖注入模式及我们在使用Spring框架中遇到的一些问题,不管能不能解决问题,先把问题摆出来再说。
可能等到我们把依赖注入模式的来龙去脉搞清楚了一些,才能避免在使用Spring的过程中像“围城”式的困境:“城外的人想进去;城里的人想出来”。
何谓“依赖注入”模式
很多关于依赖注入和Spring的书籍和文章,在谈论依赖注入模式和IOC容器的时候,喜欢说它们实现了“插件”式的编程。这只是站在使用者的角度来泛泛的谈论依赖注入模式及IOC容器的功能,而没有全面的剖析依赖注入模式和IOC容器的原理;即使是谈论它们的功能,在我看来,这句话也是不全面的,因为能够实现“插件”式编程的模式很多,比如工厂模式,而依赖注入模式和IOC容器不但能实现“插件”式编程,而且通过他们向服务注入依赖关系。这两个功能,对于依赖注入模式和IOC容器来说,才是一个全面的理解。
那么,到底依赖注入模式和IOC容器的工作原理是什么样的呢?
大家都公认的是,设计模式首先是一种思想。既然是思想,就有一定的覆盖性,所以依赖注入模式也能从现实生活中找到例子。
我们要给出的例子是大家都很熟悉的生活中的例子。借贷的例子在生活中比比皆是,谁都会有在工作和生活缺少资金的时候,这时候借贷就是我们的一个选择了。
假设我们在银行没有产生以前,我们该怎么借贷呢?下面的图描述了这种情况:
![](http://blog.csdn.net/images/blog_csdn_net/hivon/clip_image002.jpg)
贷款人员需要许诺给借贷人员一定的利息,即注入一定量的息钱,才能借到款项。
同理,在软件过程中,我们可能在使用某一个服务的时候,需要向该服务注入某些依赖,才能使用这个服务。其原理图如下:
![](http://blog.csdn.net/images/blog_csdn_net/hivon/clip_imge002.jpg)
图中,服务的使用者首先向服务注入一定的依赖,然后就可以从服务那里使用该服务了。
从借贷流程(服务使用流程)中,我们可以看出,这种贷款人员(服务的使用者)和借贷人员(服务)之间的直接关系,对于贷款人员(服务的使用者)是很不利的:首先贷款人员必须亲自找到借贷人员。反映在我们软件过程中的服务使用上,我们必须知道如何初始化一个服务,这增加了我们使用服务的难度。其次,贷款人员需要把利息支付给借贷人员,即需要知道如何给借贷人员支付利息(借贷人员的爱好各不相同,有的喜欢贷款人员用金子支付,有的喜欢古董……)。同样,在我们软件过程中的服务使用上,我们也必须知道如何把依赖注入到服务中去,是通过构造器注入,还是设置注入,或者接口注入,这些都要看服务的开发人员的喜好。
通过以上的两点,可以看出,对于贷款人员或服务的使用者来说,直接和借贷人员或者服务打交道是不好,或者说不方便的。
对于借贷来说,我们现在有了银行,贷款人员不用直接跟借贷人员打交道。其硫程如下:
![](http://blog.csdn.net/images/blog_csdn_net/hivon/cliimage002.jpg)
上面的流程是十分清晰的,借贷人员和贷款人员都只和银行打交道,而不必直接打交道。在这个过程中,我们引入了一个重要概念:契约。正是这个契约,才使得借贷人员和贷款人员在和银行打交道的过程中变得有章可循。
现在,我们来看有了银行这个中间层,对于贷款人员的好处:第一,贷款人员直接向银行贷款,他可以不必管谁是借贷人员,对借贷人员可以一无所知。第二,贷款人员付利息只需满足银行的要求,而不必理会某个借贷人员的奇怪要求,这是银行和借贷人员之间的事。
同理,我们来看有了IOC容器下的对服务的使用,如下图:
![](http://blog.csdn.net/images/blog_csdn_net/hivon/clip_image005.jpg)
同样是引入了契约,这个契约正是我们所谓“插件”式编程的基础。服务的使用者提供契约,向IOC容器注入依赖;IOC容器再根据契约向服务注入依赖。这样,IOC容器取得了服务对象;最后将该服务对象传递给服务的使用者,服务的使用者取得了服务对象。
有了IOC容器,好处和有了银行的好处是一样的:第一,服务的使用者可以不必关心如何初始化服务。第二,服务的使用者只需要提供依赖,而不必关心到底怎么样将依赖注入到服务中去。
除此之外,依赖注入模式或IOC容器还有一个大而化之的好处:可以把所有的服务集中管理,使用者只需到契约中寻找相应的服务即可。这就是“插件”式编程,开发服务的时候可以不必理会服务的使用者,只需在服务开发完成之后,通过契约“插入”到IOC容器即可。同时,服务的使用者也可以不必管服务是否完成,在客户端完成以后,通过契约“插入”到IOC容器即可。如果那天客户端觉得这个服务不好,想换个服务,只需改动它和IOC容器的契约就行。当然,前提条件是IOC容器能够提供客户端所需要的服务。
上面我们泛泛的谈论了依赖注入模式和IOC容器的好处。现在我们通过一个代码实例来看看我们的实际编码是怎么产生依赖注入模式和IOC容器的需求的。
这个例子很简单,假设我们需要做这样一个功能:将一个由简单类型元素组成的容器里面的元素打印出来。代码如下:
public class CollectionPrinter {
private ArrayList list;
public CollectionPrinter()
{
this.list = new ArrayList();
this.list.add("1");
this.list.add("2");
this.list.add("3");
}
public void printAll()
{
for(int i=0;i
{
System.out.println(this.list.get(i).toString());
}
}
}
在这个类里,我们首先定义了一个ArrayList对象,然后在类的构造器里将这个对象初始化,最后在printAll()方法里将list对象的元素一一打印出来。
我们可以看到,CollectionPrinter类完全依赖于ArrayList类,这使得本来能够被公用的方法:printAll()不能够被公用。所以我们需要对CollectionPrinter类对ArrayList类的依赖进行解耦,解除CollectionPrinter类对ArrayList类的依赖,使得该类能够被重用。
首先,我们根据依赖颠倒原则,让CollectionPrinter类依赖于ArrayList类的抽象。修改如下:
public class CollectionPrinter {
private Iterator it;
public CollectionPrinter()
{
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
this.it = list.iterator();
}
public void printAll()
{
while(this.it.hasNext())
{
System.out.println((String)it.next());
}
}
}
这个类现在已经依赖了容器类的抽象:Iterator接口。但仍然不够,因为它还依赖于ArrayList类,我们需要在构造器里实例化一个ArrayList对象。这样,CollectionPrinter类显然还有没从对ArrayList类的依赖中独立出来。但是,如果我们把构造器作如下改动:
public CollectionPrinter(Iterator it)
{
this.it = it;
}
这样的话,CollectionPrinter类就彻底从对ArrayList类的依赖中独立出来了。那么,这个类的使用者就可以这样使用了:
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
CollectionPrinter cp = new CollectionPrinter(list.iterator());
cp.printAll();
这是我们一个对问题的一般的解决思路。可以说,上面的CollectionPrinter类已经具有了相当的扩展性。但仍然有一些问题需要提出来:
第一,
使用者需要知道如何对CollectionPrinter进行实例化。
第二,
使用者需要知道如何将Iterator对象传给该类,如需要知道是在构造器里传入还是在printAll方法里传入。
如果把CollectionPrinter看成一个组件或服务,上面的问题的实质是组件或服务的使用者如何和组件或服务打交道的问题。使用者当然是希望这种交道越简单越好。
正是上面的两个问题,向我们提出来这样一种服务的需求:这种服务能够帮助我们把客户的依赖对象注入到我们所需要的组件或服务里面去,客户端只是简单的取得了组件或服务对象,就能够使用该组件或服务的对象。
根据上面的例子,我们需要这样的一个类:我们把list.iterator()对象给这个类,然后这个类给我们产生CollectionPrinter对象,而我们不需要管到底是将list.iterator()对象赋值到CollectionPrinter类的那个地方。
下面将模拟一个有简单的依赖注入功能的这样一个类:
public static class ClassContainer
{
public CollectionPrinter getInstance(Iterator it)
{
return new CollectionPrinter(it);
}
}
这个类基本上解决了上面所提出的两个问题,但是没有太多的使用价值。因为在一个项目中,像这样有依赖注入需求的类很多,这一个有这样需求的类如果都需求这样一个提供依赖注入需求的类,那就不是简化了代码,而是使代码复杂和冗余化了。
假如我们同时有这么一个类,用来判断Iterator对象中的某一个字符串,如下:
public class CollectionFinder
{
private Iterator it;
public CollectionFinder(Iterator it)
{
this.it = it;
}
public boolean find(String str)
{
while(this.it.hasNext())
{
if(((String)it.next()).equals(str)) return true;
}
return false;
}
}
所以,为了我们的ClassContainer类在功能上需要进一步的扩展,以同时满足CollectionPrinter类和CollectionFinder类的依赖注入的要求。
public class ClassContainer
{
public static Object getInstance(String className,Iterator it)
{
try
{
Class cls = Class.forName(className);
Class[] types = new Class[]{Iterator.class};
Constructor ctor = cls.getConstructor(types);
Object[] obj = new Object[]{list.iterator()};
return ctor.newInstance(obj);
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
}
这个类同时满足了CollectionPrinter类和CollectionFinder类的依赖注入的要求,但仍然有不足之处。大家很容易的看来:假如有一个类,需要注入的依赖的类型不是Iterator类型,这个ClassContainer类仍然不能满足我们的要求,需要做进一步的优化。
public class ClassContainer
{
public static Object getInstance(String className,String type,Object value)
{
try
{
Class cls = Class.forName(className);
Class[] types = new Class[]{Class.forName(type)};
Constructor ctor = cls.getConstructor(types);
Object[] obj = new Object[]{value};
return ctor.newInstance(obj);
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
}
现在,这个ClassContainer类看起来就有那么一定的IOC容器的模样了。我们对CollectionPrinter类和CollectionFinder类的调用就变成了下面的样子:
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
//对CollectionPrinter类的调用
Object obj = ClassContainer.getInstance(“CollectionPrinter”,” java.util.Iterator”,list.iterator());
If(obj!=null)
{
((CollectionPrinter)obj).printAll();
}
//对CollectionFinder类的调用
Object obj1 = ClassContainer.getInstance(“CollectionFinder”,” java.util.Iterator”,list.iterator());
If(obj!=null)
{
System.out.println(((CollectionFinder)obj).find(“a”));
}
如果再对ClassContainer类引入契约,以xml.文件的形式来定义需要从ClassContainer中取得的类以及它们所需要依赖的参数类型和参数,那么这个ClassContainer在形式上就和IOC容器更加的相像了。
当然,ClassContainer类要真正完全实现IOC容器的功能,需要对它在功能上作进一步的扩展,以适应不同的类对依赖注入的不同的要求。
好了,关于依赖注入模式和IOC容器的需求的提出以及它们的实现原理就说这么多。大家有什么想法,欢迎提出来,我们可以作进一步的讨论。
在本系列的下一部分,我们将谈谈我们在使用依赖注入模式和IOC容器的实现,即Spring的时候遇到的问题和产生的困惑,以期进一步搞清楚我们该如何使用Spring的依赖注入模式。