“围城”式困境中的依赖注入模式及Spring(3)
“围城”式困境中的依赖注入模式及Spring(3) 依赖注入模式和工厂模式由于依赖注入模式的功能之一是初始化一个类,所以可以通过依赖注入模式来获取对象。我们知道,这个功能一向是工厂模式的领地。于是,有很多人就公然宣称:依赖注入模式能够代替工厂模式。他们的理由之一是,既然有了IOC容器,就可以通过它来初始化
·
“围城”式困境中的依赖注入模式及Spring(3)
依赖注入模式和工厂模式
由于依赖注入模式的功能之一是初始化一个类,所以可以通过依赖注入模式来获取对象。我们知道,这个功能一向是工厂模式的领地。于是,有很多人就公然宣称:依赖注入模式能够代替工厂模式。他们的理由之一是,既然有了IOC容器,就可以通过它来初始化对象。何必再用工厂模式呢?因为如果用工厂模式的话,我们得多增加一个工厂类。
同时,IOC模式的狂热追随者宣称:工厂模式只能获得特定接口的类的实例;而IOC模式则可以获取任意类的对象。由此可以看出,IOC模式比工厂模式更加灵活。因此IOC模式可以取代工厂模式。
可事实真的是这样的吗?
假如我们有五种动物:Checken,Sheep,Cat,Dog,Pig;它们都继承Animal接口,如下:
public interface Animal
{
public void eat();
public String say();
}
其中,Checken类的实现如下:
public class Checken implements Animal
{
public void eat()
{
System.out.println(“The checken cats insert……”);
}
public String say()
{
System.out.println(“The checken’s saying sounds like crow……”);
return “crow”;
}
}
其他,关于Sheep类,Cat类,Dog类和Pig类的实现在这里不再给出,它们都实现了Animal接口。
如果我们用IOC容器来获取各个类的实例,这里以Spring的IOC容器为例,一般要经过如下的两步的配置:
<bean id="checkenTarget" class="Checken">
<constructor-arg>
<ref bean="AppCtx" />
</constructor-arg>
</bean>
<bean id="checken" parent="parentService">
<property name="target">
<ref bean=" checkenTarget " />
</property>
</bean>
以上是关于Checken类的配置,要完全实现上述的五个类的IOC容器的配置,还需要和上面雷同的四个配置。如下:
<bean id="sheepTarget" class="Sheep">
<constructor-arg>
<ref bean="AppCtx" />
</constructor-arg>
</bean>
<bean id="sheep" parent="parentService">
<property name="target">
<ref bean="sheepTarget " />
</property>
</bean>
<bean id="catTarget" class="Cat">
<constructor-arg>
<ref bean="AppCtx" />
</constructor-arg>
</bean>
<bean id="cat" parent="parentService">
<property name="target">
<ref bean=" catTarget " />
</property>
</bean>
<bean id="dogTarget" class="Dog">
<constructor-arg>
<ref bean="AppCtx" />
</constructor-arg>
</bean>
<bean id="dog" parent="parentService">
<property name="target">
<ref bean="dogTarget " />
</property>
</bean>
<bean id="pigTarget" class="Pig">
<constructor-arg>
<ref bean="AppCtx" />
</constructor-arg>
</bean>
<bean id="pig" parent="parentService">
<property name="target">
<ref bean="pigTarget " />
</property>
</bean>
这样,这五个类的IOC容器的配置才算完成。上面的五个类如果用简单工厂模式来实现的话,代码如下:
public class Factory
{
public static Animal getInstance(String type)
{
if(“checken”.equals(type))
{
return new Checken();
}
else if(“sheep”.equals(type))
{
return new Sheep();
}
else if(“cat”.equals(type))
{
return new Cat();
}
else if(“dog”.equals(type))
{
return new Dog();
}
else
{
return new Pig();
}
}
}
把两者加以比较,可以看出,简单工厂模式的工厂类的编写,的确要比IOC容器的繁琐的配置要来得简单得多。
如此,我们可以看到使用IOC容器的第一个不便:繁琐复杂的配置管理,这样的配置既枯燥乏味,又容易出错。经常需要我们拷贝来拷贝去,但仍然保证不了不出错。
我们再来看一看我们是如何在IOC容器里取得实例的:
String[] contextFiles = null;
if(contextFiles == null)
{
contextFiles = new String[]{"applicationContext.xml",
"dataAccessContext-local.xml"};
}
appCtx = new ClassPathXmlApplicationContext(contextFiles);
Animal checken = (Animal)appCtx.getBean("checken");
而工厂模式又是如何取得各个类的实例的呢?
Animal checken = Factory.getInstance(“checken”);
我们可以看到,使用工厂模式取得类的实例非常简单,而使用IOC容器则由于容器的普适性,需要对取得的对象进行强制类型转化,以获得我们所需要的Animal类型。这是IOC容器的使用不如工厂模式方便的第二个方面。
第三个方面:所有的所谓的“插件式”编程所带来的同一个问题,IOC容器同样不能避免,当在工厂类我们误将Checken类的类名写错,如下:
if(“checken”.equals(type))
{
return new Check();
}
那么Factory类编译的时候就会出错,很显然,这有利于我们修改错误。但是如果我们在IOC容器的配置文件中写错,如下:
<bean id="checkenTarget" class="Check">
<constructor-arg>
<ref bean="AppCtx" />
</constructor-arg>
</bean>
<bean id="checken" parent="parentService">
<property name="target">
<ref bean=" checkenTarget " />
</property>
</bean>
那么IDE在编译的时候是检查不出错误来的,只能是在运行的时候抛出ClassNotFoundException这样的违例来告诉我们出了错。很显然,这种查错方式不如编译的时候方便。
上面的讨论可以看出,依赖注入模式,或者说IOC容器在实现对依赖注入对象的普适性的时候,给我们引入了配置管理的麻烦。
同时,“插件式”的编程可以使我们很轻松的把一个系统的两个部分在运行期内耦合到一起,但也给我们带来了一些本该在编译器就调试出来的错误直到运行期内才能被发现,而且查错也没有编译期那么容易。
而工厂模式则可以避免这些麻烦,所以工厂模式和依赖注入模式两者都有他们不同的适用范围。但是它们之间的区别又是非常微妙的,需要我们来细细的品位。
首先,如果产品是设计者能够把握的有限几种,如上面的Animal接口的几个实现,系统设计完成之后,可预期的产品扩展的可能性比较小。也就是除了上面的五个类以外,不大可能扩展出新的类来。这时候用工厂模式比较好,省去使用IOC容器所带来的配置麻烦。
如果想实现“插件式”编程,即把前后台分给不同的开发者或开发小组去同时实现。这时候,IOC容器是一个不二的选择。工厂模式是无能为力的。
除此之外,依赖注入模式和工厂模式还有一个重要的区别:就是工厂模式只关心生产产品,而对依赖的注入不太关心,或者说工厂模式对依赖注入的处理能力稍显不足。而依赖注入模式则强调了依赖的注入。两者的区别是工厂模式中变化的是产品,即有多种的产品;而依赖注入模式中变化的是依赖,即有千变万化的依赖。
就上面的例子,假如你只想听到各种动物的叫声,那么你只关心如何取得产品,则使用工厂模式比较方便,代码如下:
Animal animal = Factory.getInstance(type);
animal.say();
但是,如果你想找出像“bark”这样的叫声的动物来:
public class AnimalManager
{
private Animal animal;
public void setAnimal(Animal animal)
{
this.animal = animal;
}
public void findBarkAnimal()
{
if(this.animal.say.equals(“bark”))
{
System.out.println(animal.Class.getName()+” is a bark animal!”);
}
}
}
你的AnimalManager类可能给你无法控制的系统使用,那些系统可能有各种各样你所无法预知的动物。这时候使用依赖注入模式比较好。
话又说回来,前面我们所说的“插件式”编程,前后台分开开发的模式,其实就是一种依赖注入的变形。它是将后台服务作为一种依赖,注入到前台的系统中去。
所以,我们在使用IOC容器的时候,总是在往容器中插入依赖,而不是单纯的获取对象。而且是依赖IOC容器将依赖注入到我们所要获取的对象中去,这才是我们使用IOC容器的最大的目的。
总之,使用工厂模式的时候,变化的是我们所要获取的对象;而使用IOC容器的时候,变化的是我们需要往获取对象中注入的依赖,而我们所要获取的对象可能是变化的,也可能不变。
依赖注入模式还有一个常用的地方:还是以上面的animal为例,如果我们在一个版本中只有一个Cat类,其配置如下。
<bean id="animalTarget" class="Cat">
<constructor-arg>
<ref bean="AppCtx" />
</constructor-arg>
</bean>
<bean id="animal" parent="parentService">
<property name="target">
<ref bean=" animalTarget " />
</property>
</bean>
我们对这个Animal的实现的使用如下:
//取得animal的对象
Animal animla = appCtx.getBean("animal");;
//对该对象进行操作
……
现在在下一个版本中,确定具体的Animal实现是要变化的,即肯定不是Cat对象了。假如是Dog对象。但是对Animal的实现的使用代码不变。
那么我们只需增加Dog对象,然后修改上面的配置文件,将<bean id="animalTarget" class="Cat">中的Cat改成Dog就行。
如果我们对Animal的使用的代码是这样的:
Animal animla = new Cat();
……
那么当我们在下一个版本需要将Cat换成Dog的话,就不得不上面的对Animal的使用代码了。
当然,你可能会说,我使用工厂模式也能做到啊。
不错,当你的系统只有一个Animal接口的时候,你使用工厂模式也可以。但如果你的系统有很多的接口,它们的实现遇到的情况也和Animal接口遇到的情况一模一样,那么你使用工厂模式还好吗?你将不得不为每一个接口做一个工厂。
啰里啰唆地说了好多,现在在总结一下吧,以此作为这一节文字的蛇足吧!
第一、
使用工厂模式的情况,工厂类的设计者可以确定有多少个产品,而且产品的扩展性不大,就是说不太可能有新的产品会添加到已经设计好的系统中。
第二、
“插件式”编程,即前后台分开并行编码的系统中,应该使用依赖注入模式。
第三、
需要往重用的代码注入依赖,而且依赖由于重用代码的使用者不同而不同,即重用代码的设计者无法控制依赖的时候,应该使用依赖注入模式。
第四、
一个系统里有很多的接口,而系统的不同的版本对同一个接口的实现的使用是相同的,而不同的版本的实现是不同的。这时候也应该使用依赖注入模式。
更多推荐
已为社区贡献1条内容
所有评论(0)