动机
将所有的内容连接在一起时应用开发的一个单调乏味的部分。有几种方式来将数据、服务、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
没有责任查找Transaction
和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
的类可以在它们的构造函数接受一个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解释了这是怎么工作的。