依赖注入还有另外一个响当当的名字:反转控制(IOC)。
大多数的应用程序都有多个类同时协同工作来完成业务逻辑。通常情况下,每个对象自己负责获取它依赖对象的引用,这往往会产生紧耦合的代码。而应用DI时,应用对象是在创建时被赋予它们的依赖性。换言之,依赖性是被注入到应用对象中。DI最关键的优势在于松耦合。这种松耦合在于将依赖性和接口相捆绑。试想,如果一个类Foo有一个接口变量Bar,那么Bar接口是如何实现的Foo类根本无需关心,Bar有可能是一个JavaBean,一个远程web Service,一个EJB或是一个单元测试中的mock实现。
下面给出了DI的第一个例子。假设我们需要一个类来表示那些寻找圣杯(Holy Grail)的圆桌武士,我们可以这样写:
package com.springtest.knight;
public class RoundTableKnight {
private String name;
private HolyGrailQuest quest;
public RoundTableKnight(String name) {
this.name = name;
quest = new HolyGrailQuest();
}
public HolyGrail embarkOnQuest() {
return quest.emback();
}
}
而代码中的HolyGrailQuest类表示圆桌武士寻找圣杯的行为
package com.springtest.knight;
public class HolyGrailQuest {
public HolyGrailQuest() {}
public HolyGrail embark() {
HolyGrail grail = null;
// Search for grail
//...
return grail;
}
}
下面开始编写相应的单元测试代码:
package com.springtest.knight;
import junit.framework.TestCase;
public class RoundTableKnightTest extends TestCase {
public void testEmbarkOnQuest() {
RoundTableKnight knight = new RoundTableKnight("Bedivere");
HolyGrail grail = knight.embarkOnQuest();
assertNotNull(grail);
assertTrue(grail.isHoly());
}
}
上面的测试代码是有问题的。你会发现,RoundTableKnightTest测试类间接地测试了HolyGrailQuest类。如果HolyGrailQuest的embark方法返回为空或是抛出GrailNotFoundException异常的话上面的测试代码就会失效了。
其实可以发现,RoundTableKnightTest类如何获得HolyGrailQuest对象是一个潜在的问题。无论通过new的方式实例化还是通过JNDI获得,我们都无法独立地来测试RoundTableKnightTest类——就像前面说的,每次测试RoundTableKnightTest类都不可避免地要测试HolyGrailQuest类。这就是所谓的紧耦合。
通常使用接口隐藏具体实现来达到解耦的目的。这样具体的实现类就可以被随意替换而不影响客户类(即使用具体类的类)的使用。按照这种思路,我们来改写上面的例子。
第一步,创建一个Quest接口
package com.springtest.knight;
public interface Quest {
Object embark() throws QuestFailedException;
}
然后令HolyGrailQuest类实现该接口。
package com.springtest.knight;
public class HolyGrailQuest implements Quest {
public HolyGrailQuest() {}
@Override
public Object embark() throws QuestFailedException {
// TODO Auto-generated method stub
return new HolyGrail();
}
}
同样,我们抽取出Knight作为一个接口。
package com.springtest.knight;
public interface Knight {
Object embarkOnQuest() throws QuestFailedException;
}
最后我们需要这样编写RoundTableKnight来实现Knight
package com.springtest.knight;
public class RoundTableKnight implements Knight {
private String name;
private Quest quest;
public RoundTableKnight(String name) {
this.name = name;
}
@Override
public Object embarkOnQuest() throws QuestFailedException {
// TODO Auto-generated method stub
return quest.embark();
}
public void setQuest(Quest quest) {
this.quest = quest;
}
}
发现不同了吗?那就是RoundTableKnight的构造函数不再负责对Quest进行实例化,而是通过一个setter方法来实现。即,RoundTableKnight不再负责获取quest对象。事实上,RoundTableKnight类将被赋予Quest对象,这就是依赖注入。
在spring中最常用的就是使用XML文件来实现依赖注入。下面就为该例子创建它的xml文件:knight.xml 它负责将一个Quest对象赋予给RoundTableKnight类。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="quest" class="com.springtest.knight.HolyGrailQuest"/>
<bean id="knight" class="com.springtest.knight.RoundTableKnight">
<constructor-arg value="spring"/>
<property name="quest" ref="quest"></property>
</bean>
</beans>
写好xml文件之后现在的任务就是要加载xml文件并开始编写应用程序。
package com.springtest.knight;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;
public class KnightApp {
public static void main(String[] args) {
// TODO Auto-generated method stub
BeanFactory factory = new XmlBeanFactory
(new FileSystemResource("knight.xml"));
Knight knight = (Knight)factory.getBean("knight");
try {
knight.embarkOnQuest();
} catch (QuestFailedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}