首先我们考虑一个简单的例子,这里我们使用engine 类和car 类。为了更加清楚的描述问题,我们将类和接口都置空。每辆car会有一个engine,我们想给car装备上著名的MooseEngine。
Engine类如下:
1 public interface Engine { 2 3 } 4 5 public class SlowEngine implements Engine { 6 7 } 8 9 public class FastEngine implements Engine {10 11 }12 13 public class MooseEngine implements Engine {14 15 }
然后我们可以得到一个car类:
1 public class Car {2 3 private MooseEngine engine;4 5 }
这是一辆非常棒的汽车,但是即使有其他种类的引擎上市,我们也不能装备这些引擎了。我们说这里的car类和MooseEngine类是紧耦合的(tightly coupled)。虽然MooseEngine很棒,但是如果我们想把它换成别的引擎呢?
回到顶部
你可能已经注意到了MooseEngine实现了Engine接口。其它引擎也实现了同样的接口。我们可以想一想,当我们设计我们的Car类时,我们想让一辆“car”装备一个“engine”。所以我们重新实现一个Car类,这次我们使用Engine接口:
1 public class Car {2 3 private Engine engine;4 5 }
接口编程是依赖注入中的一个很重要的概念。我听到了你的尖叫,“等一下,你在这里使用接口,具现类(concrete class)该怎么办?你在哪里设置(set)引擎?我想在我的汽车中装备MooseEngine”。我们可以按下面的方式来设置它:
1 public class Car {2 3 private Engine engine = new MooseEngine();4 5 }
但这就是有用的么?它看上去和第一个例子没有多大区别。我们的car仍然同MooseEngine是紧耦合的。那么,我们该如何设置(set或者说注入(inject))我们的汽车引擎呢?
回到顶部
就像依赖注入这个名字一样,依赖注入就是注入依赖,或者简单的说,设置不同实例之间的关系。一些人将它同好莱坞的一条规矩关联了起来,“不要给我打掉话,我打给你。”我更喜欢叫它“bugger”法则:“我不关心你是谁,按我说的做。”在我们的第一个例子中,Car依赖的是Engine的具现类MooseEngine。当一个类A依赖于另外一个类B的时候,类B的实现直接在类A中设置,我们说A紧耦合于B。第二个例子中,我们决定使用接口来代替 具现类MooseEngine,这样就使得Car类更加灵活。并且我们决定不去定义engine的具现类实现。换句话说,我们使Car类变为松耦合(loosely coupled)的了。Car不再依赖于任何引擎的具现类了。那么在哪里指定我们需要使用哪个引擎呢?依赖注入该登场了。我们不在Car类中设置具现化的Engine类,而是从外面注入。这又该如何实现呢?
设置依赖的一种方法是把依赖类的具体实现传递给构造函数。Car类将会变成下面这个样子:
1 public class Car { 2 3 private Engine engine; 4 5 public Car(Engine engine) { 6 7 this.engine = engine; 8 9 }10 11 }
然后我们就可以用任何种类的engine来创建Car了。例如,一个car使用MooseEngine,另外一个使用crappy SlowEngine:
1 public class Test { 2 3 public static void main(String[] args) { 4 5 Car myGreatCar = new Car(new MooseEngine()); 6 7 Car hisCrappyCar = new Car(new SlowEngine()); 8 9 }10 11 }
另外一种设置依赖的普通方法就使用setter方法。当需要注入很多依赖的时候,建议使用setter方法而不是构造函数。我们的car类将会被实现成下面的样子:
1 public class Car { 2 3 private Engine engine; 4 5 public void setEngine(Engine engine) { 6 7 this.engine = engine; 8 9 }10 11 }
它和基于构造函数的依赖注入非常类似,于是我们可以用下面的方法来实现上面同样的cars:
1 public class Test { 2 3 public static void main(String[] args) { 4 5 Car myGreatCar = new Car(); 6 7 myGreatCar.setEngine(new MooseEngine()); 8 9 Car hisCrappyCar = new Car();10 11 hisCrappyCar.setEngine(new SlowEngine());12 13 }14 15 }
回到顶部
如果你将Car类的第一个例子同使用setter依赖注入的例子进行比较,你可能认为后者使用了额外的步骤来实现Car类的依赖注入。这没错,你必须实现一个setter方法。但是当你在做单元测试的时候,你会感觉到这些额外的工作都是值得的。如果你对单元测试不熟悉,推荐你看一下这个帖子单元测试有毒 。我们的Car的例子太简单了,并没有把依赖注入对单元测试的重要性体现的很好。因此我们不再使用这个例子,我们使用前面已经讲述过的关于篝火故事的例子,特别是在在单元测试中使用mock中的部分。我们有一个servlet类,通过使用远端EJB来在农场中”注册”动物:
1 public class FarmServlet extends ActionServlet { 2 3 public void doAction( ServletData servletData ) throws Exception { 4 5 String species = servletData.getParameter("species"); 6 7 String buildingID = servletData.getParameter("buildingID"); 8 9 if ( Str.usable( species ) && Str.usable( buildingID ) ) {10 11 FarmEJBRemote remote = FarmEJBUtil.getHome().create();12 13 remote.addAnimal( species , buildingID );14 15 }16 17 }18 19 }
你已经注意到了FarmServlet被紧耦合到了FarmEJBRemote实例中,通过调用“FarmEJBUtil.getHome().create()”来取回实例值。这么做会非常难做单元测试。当作单元测试的时候,我们不想使用任何数据库。我们也不想访问EJB服务器。因为这不仅会使单元测试很难进行而且会使其变慢。所以为了能够顺利的为FarmServlet类做单元测试,最好使其变成松耦合的。为了清除FarmServlet和FarmEJBRemote之间的紧依赖关系,我们可以使用基于setter的依赖注入:
1 public class FarmServlet extends ActionServlet { 2 3 private FarmEJBRemote remote; 4 5 public void setRemote(FarmEJBRemote remote) { 6 7 this.remote = remote; 8 9 } 10 11 public void doAction( ServletData servletData ) throws Exception {12 13 String species = servletData.getParameter("species");14 15 String buildingID = servletData.getParameter("buildingID");16 17 if ( Str.usable( species ) && Str.usable( buildingID ) ) {18 19 remote.addAnimal( species , buildingID );20 21 }22 23 }24 25 }
在真实的部署包中,我们确保通过调用“FarmEJBUtil.getHome().create()”而创建的一个FarmServlet远端成员实例会被注入。在我们的单元测试中,我们使用一个虚拟的mock类来模拟FarmEJBRemote。换句话说,我们通过使用mock类来实现FarmEJBRemote:
1 class MockFarmEJBRemote implements FarmEJBRemote { 2 3 private String species = null; 4 5 private String buildingID = null; 6 7 private int nbCalls = 0; 8 9 public void addAnimal( String species , String buildingID )10 11 {12 13 this.species = species ;14 15 this.buildingID = buildingID ;16 17 this.nbCalls++;18 19 }20 21 public String getSpecies() {22 23 return species;24 25 }26 27 public String getBuildingID() {28 29 return buildingID;30 31 }32 33 public int getNbCalls() {34 35 return nbCalls;36 37 }38 39 }40 41 42 43 public class TestFarmServlet extends TestCase {44 45 public void testAddAnimal() throws Exception {46 47 // Our mock acting like a FarmEJBRemote48 49 MockFarmEJBRemote mockRemote = new MockFarmEJBRemote();50 51 // Our servlet. We set our mock to its remote dependency52 53 FarmServlet servlet = new FarmServlet();54 55 servlet.setRemote(mockRemote);56 57 58 59 // just another mock acting like a ServletData60 61 MockServletData mockServletData = new MockServletData(); 62 63 mockServletData.getParameter_returns.put("species","dog");64 65 mockServletData.getParameter_returns.put("buildingID","27");66 67 68 69 servlet.doAction( mockServletData );70 71 assertEquals( 1 , mockRemote.getNbCalls() );72 73 assertEquals( "dog" , mockRemote.getSpecies() );74 75 assertEquals( 27 , mockRemote.getBuildingID() );76 77 }78 79 }
这样很容易就能测试FarmServlet了。
另外有需要云服务器可以了解下创新互联scvps.cn,海内外云服务器15元起步,三天无理由+7*72小时售后在线,公司持有idc许可证,提供“云服务器、裸金属服务器、高防服务器、香港服务器、美国服务器、虚拟主机、免备案服务器”等云主机租用服务以及企业上云的综合解决方案,具有“安全稳定、简单易用、服务可用性高、性价比高”等特点与优势,专为企业上云打造定制,能够满足用户丰富、多元化的应用场景需求。