Google Guice 用户指南 - Ⅱ:愿景

发布时间 2023-11-14 23:18:24作者: kefate

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

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