为什么选择Guice框架

使用Guice框架的动机

在应用中组装各个封装好的类,有时候是一件很乏味的事情。有几种办法可以把数据层、业务层、表现层的代码整合在一起。下面通过一个在线披萨下订单的业务来对比这几种实现方法。

// 定义下订单接口
public interface BillingService {

  /**
   * Attempts to charge the order to the credit card. Both successful and
   * failed transactions will be recorded.
   *
   * @return a receipt of the transaction. If the charge was successful, the
   *      receipt will be successful. Otherwise, the receipt will contain a
   *      decline note describing why the charge failed.
   */
  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());
    }
  }
}

上面代码的主要问题是没有进行模块化,也不好测试。不然,你测试的时候,真的要从你的信用卡扣费了。而且,也很难测试支付失败,或者支付网关服务不可用的情况。

使用工厂方法

工厂方法解耦了调用类跟实现类之间的耦合。一般工厂方法使用静态的set跟get方法来设置跟获取实现类。如下面的示例代码:

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

上面代码看起来也很笨拙。因为使用全局变量来保存模拟支付类FakeCreditCardProcessor的实例,需要在teardown对于全局变量进行释放。如果teardown执行失败,而且后面的测试也用到了这个变量,会对后面的测试造成影响。同样,由于全局变量的污染,也无法进行并行测试。

最严重的问题是,所有的依赖都隐藏码在代码中了。比如,在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());
  }
}

如果我们在RealBillingService增加依赖,编译器会提醒我们哪个测试方法需要被修复了。

但是现在BillingService的使用者需要知道它的依赖,并在构造方法中传入这些依赖,通常在一个入口类传入。

public static void main(String[] args) {  
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();
    BillingService billingService
        = new RealBillingService(processor, transactionLog);
    ...
  }

使用Guice来实现依赖注入

依赖注入让设计模式让代码可以模块化、可测试化,Guice让依赖注入的代码更容易书写。

在上面的例子中使用Guice的话,我们首先要告诉Guice怎么通过接口找到它的实现类。我们通过一个实现了Guice Module接口的配置类来完成这个工作。

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

我们通过添加@InjectRealBillingService的构造方法,让Guice能够找到它的依赖。

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

官方文档链接:https://github.com/google/guice/wiki/Motivation