譯者:kefate 原文:https://github.com/google/guice/wiki/Motivation 將所有組件連接在一起是應用程式開發中繁瑣的一部分。有多種方法可以將數據、服務和表示層類連接在一起。為了對比這些方法,我們將編寫一個披薩訂購網站的計費代碼: public inte ...
譯者:kefate
將所有組件連接在一起是應用程式開發中繁瑣的一部分。有多種方法可以將數據、服務和表示層類連接在一起。為了對比這些方法,我們將編寫一個披薩訂購網站的計費代碼:
public interface BillingService {
/**
* 嘗試使用信用卡對訂單進行扣款。成功和失敗的交易都將被記錄
*
* @return 交易的收據。如果扣款成功,收據將顯示成功信息;否則,收據將包含一份拒絕說明,描述為何扣款失敗。
*/
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());
}
}
}
這段代碼帶來了模塊化和可測試性的問題。這種對真實的信用卡處理器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
不負責查找 TransactionLog
和 CreditCardProcessor
對象,而是把他們作為構造函數參數傳遞進來:
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());
}
}
}
既然我們現在不需要任何工廠類,那麼通過刪除單測的 setUp
和 tearDown
方法,我們就可以簡化單測:
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);
...
}
下一節我們將會介紹這一切是如何運作的。