為什麼選擇Guice框架

白話IT發表於2019-04-15

使用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);
    ...
  }
複製程式碼

原文連結: www.wetimer.com/wei-shi-yao…

相關文章