【翻譯】 Guice 動機——依賴注入的動機

Dybvig發表於2019-02-08

原文連結

動機

將所有的內容連線在一起時應用開發的一個單調乏味的部分。有幾種方式來將資料、服務、presetntation類連線到一起。為了對比這些方法,我將為披薩訂購網站編寫賬單程式碼:

public interface BillingService {
    // 嘗試在信用卡中扣除訂單的費用。成功和失敗的交易都會被記錄
    Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

伴隨著實現,我們將為我們的程式碼編寫單元測試。在測試中,我們需要一個FakeCreditCardProcessor來避免從真實的信用卡扣費!

直接建構函式呼叫

以下是,當我們只是new一個信用卡處理器和一個交易日誌時,程式碼的樣子:

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

該程式碼給模組化和可測試性帶來問題。對真實信用卡處理器的直接編譯時依賴意味著測試程式碼將從信用卡中扣費。當發生扣費被拒絕或者當服務不可用的事情時,對測試是很不方便的。

工廠

工廠類可以解耦客戶端程式碼和實現類。一個簡單工廠使用靜態方法來獲取和設定介面的模式實現。一個工廠使用一些樣板程式碼實現:

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

在我們的客戶端程式碼中,我們只是用工廠查詢代替了呼叫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());
  }
}

上面的程式碼是笨拙的。一個全域性變數持有模擬實現,所以我們需要關心設定和清理模擬實現的操作。如果撕除失敗,那個全域性變數將會繼續指向我們的測試實列。這可能會倒是其他的測試出現問題。它還阻止我們並行執行多個測試。

但是最大的問題是依賴關係被隱藏在了程式碼中。如果我們在CreditCardFraudTracker上新增一個依賴項,那麼我們不得不重新執行測試來找出哪個依賴關係被破環了。如果我們忘了為正常服務,我們在嘗試扣費前是不會發現這個錯誤的。隨著應用的增長,維護這些工廠會變得越來越耗費生產力。
質量問題會被QA和功能測試發現。那或許就足夠了,但是我們無疑可以做的更好。

依賴注入

像工廠模式一樣,依賴注入只是一個設計模式。核心原則是:將行為從依賴解決中分離。在我們的例子中,RealBillingService沒有責任查詢TransactionCreditCardProcessor。相反,它們作為建構函式引數傳入:

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的類可以在它們的建構函式接受一個BillingService。對於頂層的類來說,有一個框架是有用的。否則,當我們需要使用一個服務時,我們將需要遞迴地構造依賴。

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

我們新增了@Inject註解到RealBillingService的建構函式,它指示Guice來使用它。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());
    }
  }
}

最後,我們可以將它們放到一起。Inject可以被用來獲取任何被繫結類的一個例項。

 public static void main(String[] args) {
    Injector injector = Guice.createInjector(new BillingModule());
    BillingService billingService = injector.getInstance(BillingService.class);
  }

Getting started解釋了這是怎麼工作的。

相關文章