也许在各位读者眼里,依赖注入和框架Spring等同的。其实依赖注入是一个可以在容器外使用的OO开发概念。同时,依赖注入(Dependency Injection)对于单元测试
也很有用。在该篇blog中,我们可以了解到:
- 什么是依赖注入(Dependency Injection)
- 如何保证类和依赖注入友好性
- 依赖注入有助于单元测试
女士们先生们,跟我一起来畅游吧。
一辆简单的车
让我们用引擎和车来构造一个简单的例子。为了清晰我们使类和接口程序体为空。
一辆车有引擎,而且我们希望这辆车装配路虎所用的引擎MooseEngine。
我们先配置引擎:
public interface Engine {
}
public class SlowEngine implements Engine {
}
public class FastEngine implements Engine {
}
public class MooseEngine implements Engine {
}
接着我们建立车:
public class Car {
private MooseEngine engine;
}
尽管这是一辆很好的车,但它并不能有任何规格的引擎,即使市场上有其他品牌的
车可以得到。我们说车类同路虎引擎类是紧耦合的。如果我们决定装配另一种引擎
,会发生什么?
用接口编程
你可能看到路虎引擎MooseEngine™已经实现了引擎接口。其它品牌引擎也实现了
相同的接口。让我们思考一下,当我们设计我们的车Car类时,我们认为一个
"Car"装配一个引擎。所以让我们重写一下Car类,让它使用接口代替具体的引擎
引用:
public class Car {
private Engine engine;
}
用接口编程在依赖注入中是一个重要的概念。你可能说这用了一个接口,那么具体
类在哪呢?你在哪给它赋值?若我想我的车用路虎引擎MooseEngine,我可以用以下方式给它赋值:
public class Car {
private Engine engine = new MooseEngine();
}
但是这有用么?这看起来和第一个方法没什么不同。我们的车还是紧紧捆绑在路虎 MooseEngine引擎上,这个公司倒闭了呢?我们去那里找到我们的引擎。
介绍依赖注入(Dependency Injection)
正如名字所示,依赖注入是关于注入依赖的,或者说设置实例间的关系的。有些人
引用好莱坞的格言解释它:"不要打电话给我,我会打给你"。我喜欢叫他"强
奸"原理:"我不关心你是谁,我只想你照我的要求做"。在我们第一个的例子
中,Car类依赖于叫MooseEngine的引擎具体类。当一个类A一来另一个类B并且B的
实现直接在A中得到引用时,我们说类A紧耦合类B。正如我们在第二版中看到的,我
们已经决定使用Engine接口代替MooseEngine具体类使Car更具灵活性。更进一步我
们决定不定义引擎的具体实现。换句话说,我们使Car类松散耦合。Car类不再依赖
于任何引擎的具体类。那么我们哪里定义我们用的是哪一个引擎呢?这就是依赖注
入发挥作用的地方。我们将从外面注入具体引擎类,代替在Car类中引用具体引擎
类。如何做到呢?继续GO!
1.使用基于构造方法的注入
建立依赖的一个方式是通过传递依赖的具体实现给构造方法来建立依赖。我们的Car类将变
为:
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
}
到那时我们能使用任何类型的引擎构建一辆车。例如,一辆使用伟大的路虎 MooseEngine而另一辆使用劣质的SlowEngine:
public class Test {
public static void main(String[] args) {
Car myGreatCar = new Car(new MooseEngine());
Car hisCrappyCar = new Car(new SlowEngine());
}
}
2. 基于setter方法的injection
建立依赖的另一种方法是使用setter方法。当很多依赖需要注入时,推荐使用setter方
法。我们的Car类这时又变成了样:
public class Car {
private Engine engine;
public void setEngine(Engine engine) {
this.engine = engine;
}
}
它看起来和基于构造方法注入长得差不多。我们使用以下方式实现相同的Car类:
public class Test {
public static void main(String[] args) {
Car myGreatCar = new Car();
myGreatCar.setEngine(new MooseEngine());
Car hisCrappyCar = new Car();
hisCrappyCar.setEngine(new SlowEngine());
}
}
单元测试中用依赖注入
如果你用Car类的第一版和基于setter方法注入的比较,那么你可能认为使用依赖注入需要使用一些额外的操作。这么说没错。你必须写一个setter方法。但是当你进行测试时,就会发现这些额外操作很有好处。若你对什么是单元测试一脸茫然的话,
你最好看一看《Unit in Action》这本书。我们的车例子很简单,所以不能看出单元
测试的依赖注入多么有用。 我们下车,考虑一个营火会的例子,特别是在单元测试
中使用mock这部分。我们有一个用远程EJB注册农场动物的的servlet类 。
public class FarmServlet extends ActionServlet {
public void doAction( ServletData servletData ) throws Exception {
String species = servletData.getParameter("species");
String buildingID = servletData.getParameter("buildingID");
if ( Str.usable( species ) && Str.usable( buildingID ) ) {
FarmEJBRemote remote = FarmEJBUtil.getHome().create();
remote.addAnimal( species , buildingID );
}
}
}
你应该也注意到FarmServlet是紧耦合于FarmEJBRemote实例,通过调用"FarmEJBUtil.getHome().create()"解析该实例。这样做很难进行测试。当进行单元
测试的时候,我们不想用任何数据库。我们也不想访问一个EJB服务器。这使进行
单元测试非常难以执行并且很慢。所以为了顺利进行FarmServlet 类的单元测试,我
们使它成为松散耦合的。为了移除FarmServlet和FarmEJBRemote的强依赖 ,我们用
基于setter方法的注入:
public class FarmServlet extends ActionServlet {
private FarmEJBRemote remote;
public void setRemote(FarmEJBRemote remote) {
this.remote = remote;
}
public void doAction( ServletData servletData ) throws Exception {
String species = servletData.getParameter("species");
String buildingID = servletData.getParameter("buildingID");
if ( Str.usable( species ) && Str.usable( buildingID ) ) {
remote.addAnimal( species , buildingID );
}
}
}
在真实的部署包中,我们将确定FarmServlet的远程成员的实例通过使用
"FarmEJBUtil.getHome().create()"已经被注入了。在我们的单元测试中,我们生成
一个moke类模拟FarmEJBRemote。换句话说,就是我们将用一个moke类实现
FarmEJBRemote:
class MockFarmEJBRemote implements FarmEJBRemote {
private String species = null;
private String buildingID = null;
private int nbCalls = 0;
public void addAnimal( String species , String buildingID )
{
this.species = species ;
this.buildingID = buildingID ;
this.nbCalls++;
}
public String getSpecies() {
return species;
}
public String getBuildingID() {
return buildingID;
}
public int getNbCalls() {
return nbCalls;
}
}
public class TestFarmServlet extends TestCase {
public void testAddAnimal() throws Exception {
// mock操作和一个FarmEJBRemote相似
MockFarmEJBRemote mockRemote = new MockFarmEJBRemote();
// servlet,我们将mock赋值给远程依赖。
FarmServlet servlet = new FarmServlet();
servlet.setRemote(mockRemote);
// 另一个象ServletData的mock
MockServletData mockServletData = new MockServletData();
mockServletData.getParameter_returns.put("species","dog");
mockServletData.getParameter_returns.put("buildingID","27");
servlet.doAction( mockServletData );
assertEquals( 1 , mockRemote.getNbCalls() );
assertEquals( "dog" , mockRemote.getSpecies() );
assertEquals( 27 , mockRemote.getBuildingID() );
}
}
就到这吧。我们能很容易的测试FarmServlet。
总结一下吧:
-
使用接口代替使用具体类来说明一个依赖。
-
避免在类中显式设置一个依赖的具体实现。
- 赋值依赖的具体实现可能有多种方式,包括基于构造方法或者setter方法的注
入。
- 依赖注入可以让单元测试增加灵活性。
以上的探讨,对各位同仁有用的话请回复一下。
凡是有该标志的文章,都是该blog博主Caoer(草儿)原创,凡是索引、收藏
、转载请注明来处和原文作者。非常感谢。