译者:kefate

原文:https://github.com/google/guice/wiki/Motivation

将所有组件连接在一起是应用程序开发中繁琐的一部分。有多种方法可以将数据、服务和表示层类连接在一起。为了对比这些方法,我们将编写一个披萨订购网站的计费代码:

public interface BillingService {  /**   * 尝试使用信用卡对订单进行扣款。成功和失败的交易都将被记录   *   * @return 交易的收据。如果扣款成功,收据将显示成功信息;否则,收据将包含一份拒绝说明,描述为何扣款失败。   */  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);}

除了实现代码之外,我们还将为我们的代码编写单元测试。在测试中,我们需要一个FakeCreditCardProcessor来避免对真实信用卡进行扣款。

直接调用构造方法

当我们仅仅使用new来创建CreditCardProcessorTransactionLog对象时,代码如下:

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 不负责查找 TransactionLogCreditCardProcessor对象,而是把他们作为构造函数参数传递进来:

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());    }  }}

既然我们现在不需要任何工厂类,那么通过删除单测的 setUptearDown 方法,我们就可以简化单测:

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);    ...  }

下一节我们将会介绍这一切是如何运作的。