译者:kefate
原文:https://github.com/google/guice/wiki/Motivation
将所有组件连接在一起是应用程序开发中繁琐的一部分。有多种方法可以将数据、服务和表示层类连接在一起。为了对比这些方法,我们将编写一个披萨订购网站的计费代码:
public interface BillingService { /** * 尝试使用信用卡对订单进行扣款。成功和失败的交易都将被记录 * * @return 交易的收据。如果扣款成功,收据将显示成功信息;否则,收据将包含一份拒绝说明,描述为何扣款失败。 */ Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);}
除了实现代码之外,我们还将为我们的代码编写单元测试。在测试中,我们需要一个FakeCreditCardProcessor
来避免对真实信用卡进行扣款。
直接调用构造方法
当我们仅仅使用new
来创建CreditCardProcessor
和 TransactionLog
对象时,代码如下:
public class RealBillingService implements BillingService { public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) { CreditCardProcessor processor = new PaypalCreditCardProcessor(); TransactionLog transactionLog = new DatabaseTransactionLog(); try { ChargeResult result = processor.charge(creditCard, order.getAmount()); transactionLog.logChargeResult(result); return result.wasSuccessful() ? Receipt.forSuccessfulCharge(order.getAmount()) : Receipt.forDeclinedCharge(result.getDeclineMessage()); } catch (UnreachableException e) { transactionLog.logConnectException(e); return Receipt.forSystemFailure(e.getMessage()); } }}
这段代码带来了模块化和可测试性的问题。这种对真实的信用卡处理器PaypalCreditCardProcessor
的编译时的直接依赖,意味着测试这段代码时将会用真实的信用卡进行扣款!并且,当向PaypalCreditCardProcessor
扣款被拒绝或者其服务不可用时,测试代码也会变得困难。
工厂类
使用工厂类可以将客户端和实现类进行解耦。下面是一个简单的工厂类,它用static
方法来获取实例对象/设置 mock 对象:
public class CreditCardProcessorFactory { private static CreditCardProcessor instance; public static void setInstance(CreditCardProcessor processor) { instance = processor; } public static CreditCardProcessor getInstance() { if (instance == null) { return new SquareCreditCardProcessor(); } return instance; }}
在我们的客户端代码中,我们只需要用工厂类的getInstance
方法替换掉原来通过new
创建对象的逻辑即可:
public class RealBillingService implements BillingService { public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) { CreditCardProcessor processor = CreditCardProcessorFactory.getInstance(); TransactionLog transactionLog = TransactionLogFactory.getInstance(); try { ChargeResult result = processor.charge(creditCard, order.getAmount()); transactionLog.logChargeResult(result); return result.wasSuccessful() ? Receipt.forSuccessfulCharge(order.getAmount()) : Receipt.forDeclinedCharge(result.getDeclineMessage()); } catch (UnreachableException e) { transactionLog.logConnectException(e); return Receipt.forSystemFailure(e.getMessage()); } }}
这种工厂类的写法让我们有能力写出一个恰当的单元测试出来:
public class RealBillingServiceTest extends TestCase { private final PizzaOrder order = new PizzaOrder(100); private final CreditCard creditCard = new CreditCard("1234", 11, 2010); private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog(); private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor(); @Override public void setUp() { TransactionLogFactory.setInstance(transactionLog); CreditCardProcessorFactory.setInstance(processor); } @Override public void tearDown() { TransactionLogFactory.setInstance(null); CreditCardProcessorFactory.setInstance(null); } public void testSuccessfulCharge() { RealBillingService billingService = new RealBillingService(); Receipt receipt = billingService.chargeOrder(order, creditCard); assertTrue(receipt.hasSuccessfulCharge()); assertEquals(100, receipt.getAmountOfCharge()); assertEquals(creditCard, processor.getCardOfOnlyCharge()); assertEquals(100, processor.getAmountOfOnlyCharge()); assertTrue(transactionLog.wasSuccessLogged()); }}
这种代码写起来就比较蠢。全局变量instance
在这里被设置成了一个 mock 对象,所以我们需要很小心去设置它的值或者把它恢复为初始值null
。如果tearDown
方法执行失败了,这个全局变量就会一直是我们创建的 mock 对象。这样可能会给其他的测试带来问题,并且我们也无法并发运行多个测试。
但最大的问题其实是依赖关系被隐藏到了代码里。如果工厂类中增加了对CreditCardFraudTracker
的依赖,我们必须要重新进行测试才能找出哪些测试会被破坏。如果测试类中忘记初始化一个工厂,那就只能在最后调用的时候才发现。随着应用程序规模的增长,时刻关注工厂类相关的代码就成了一件很折磨人的事情。
虽然QA会保证软件的质量,但我们身为研发,还是要尽可能地做得更好一些。
依赖注入
就像工厂模式一样,依赖注入其实也只是一种设计模式。其核心原则是将行为与依赖进行解耦。在我们的示例中,RealBillingService
不负责查找 TransactionLog
和 CreditCardProcessor
对象,而是把他们作为构造函数参数传递进来:
public class RealBillingService implements BillingService { private final CreditCardProcessor processor; private final TransactionLog transactionLog; public RealBillingService(CreditCardProcessor processor, TransactionLog transactionLog) { this.processor = processor; this.transactionLog = transactionLog; } public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) { try { ChargeResult result = processor.charge(creditCard, order.getAmount()); transactionLog.logChargeResult(result); return result.wasSuccessful() ? Receipt.forSuccessfulCharge(order.getAmount()) : Receipt.forDeclinedCharge(result.getDeclineMessage()); } catch (UnreachableException e) { transactionLog.logConnectException(e); return Receipt.forSystemFailure(e.getMessage()); } }}
既然我们现在不需要任何工厂类,那么通过删除单测的 setUp
和 tearDown
方法,我们就可以简化单测:
public class RealBillingServiceTest extends TestCase { private final PizzaOrder order = new PizzaOrder(100); private final CreditCard creditCard = new CreditCard("1234", 11, 2010); private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog(); private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor(); public void testSuccessfulCharge() { RealBillingService billingService = new RealBillingService(processor, transactionLog); Receipt receipt = billingService.chargeOrder(order, creditCard); assertTrue(receipt.hasSuccessfulCharge()); assertEquals(100, receipt.getAmountOfCharge()); assertEquals(creditCard, processor.getCardOfOnlyCharge()); assertEquals(100, processor.getAmountOfOnlyCharge()); assertTrue(transactionLog.wasSuccessLogged()); }}
现在,每当我们添加或删除依赖关系时,编译器会提醒我们哪些测试需要修正。依赖关系暴露到了API签名里。
不幸的是,现在用到BillingService
的客户端需要查找它的依赖关系。我们可以通过再次应用该模式来解决一些问题——依赖于它的类可以在它们的构造函数中接受一个BillingService
。不难发现,对于顶层的那些类来说,最好能有一个框架来完成依赖注入这件事。否则,当你需要使用一个服务时,你就需要递归地构建依赖关系。
public static void main(String[] args) { CreditCardProcessor processor = new PaypalCreditCardProcessor(); TransactionLog transactionLog = new DatabaseTransactionLog(); BillingService billingService = new RealBillingService(processor, transactionLog); ... }
使用 Guice 进行依赖注入
使用依赖性注入这种设计模式提高了代码的模块化和可测试性,而 Guice 框架使其易于编写。
为了在我们的计费例子中使用 Guice,首先,我们需要告诉它如何将接口与实现类进行映射。通过一个实现了Module
接口的 Java 类,就能完成这项配置。
public class BillingModule extends AbstractModule { @Override protected void configure() { bind(TransactionLog.class).to(DatabaseTransactionLog.class); bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class); bind(BillingService.class).to(RealBillingService.class); }}
接下来,我们只要在RealBillingService
的构造函数中加入@Inject
就可以告诉 Guice 进行处理。Guice将检查使用了@Inject
注解的构造函数,并为每个参数查找实际的值。
public class RealBillingService implements BillingService { private final CreditCardProcessor processor; private final TransactionLog transactionLog; @Inject public RealBillingService(CreditCardProcessor processor, TransactionLog transactionLog) { this.processor = processor; this.transactionLog = transactionLog; } public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) { try { ChargeResult result = processor.charge(creditCard, order.getAmount()); transactionLog.logChargeResult(result); return result.wasSuccessful() ? Receipt.forSuccessfulCharge(order.getAmount()) : Receipt.forDeclinedCharge(result.getDeclineMessage()); } catch (UnreachableException e) { transactionLog.logConnectException(e); return Receipt.forSystemFailure(e.getMessage()); } }}
最后,我们把这些打包到一起,使用Injector
即可获得任何一个映射的类实例。
public static void main(String[] args) { Injector injector = Guice.createInjector(new BillingModule()); BillingService billingService = injector.getInstance(BillingService.class); ... }
下一节我们将会介绍这一切是如何运作的。