原文链接:https://github.com/google/guice/wiki/Motivation
Motivation
将所有东西整合到一起是开发中一项乏味的工作,现在有多种方法将不同的数据、服务和展现层互相联系起来。为了对比这些不同的方法, 我们为一个披萨在线订购网站编写了计费代码
public interface BillingService {
/**
* 尝试通过信用卡支付,无论是否成功都将被记录下来
* @return 支付成功时返回成功信息,否则,返回失败原因
*
*/
Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}
在实现这个接口之前,我们要写一个单元测试。在测试中我们需要一个FakeCreditCardProcessor,因为我们不能真的从一张信用卡中刷钱=。=
Direct constructor calls
下面展示了如果我们只new一个信用卡processor和transaction logger我们的代码会是神马样
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());
}
}
}
如上代码耦合性很高,并且不易测试。单元测试里不可能对一张真实的信用卡进行操作。
Factories
一个工厂类解耦了客户端和它的实现类。一个简单的工厂使用静态方法来get和set一个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;
}
}
在我们的客户端代码中,我们只是在原先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());
}
}
}
有了工厂,我们就可以实现一个正确的UT
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());
}
}
这看起来很笨拙。一个全局变量持有mock实现,所以在设置和销毁时我们需要很小心。如果tearDown失败了,这个全局变量将会继续指向我们的测试实例,这将会对其他的UT产生影响,我们也不能并行地运行多个测试用例。
但是,最大的问题在于,依赖被隐藏在代码里。如果我们要添加一个新的依赖CreditCardFraudTracker,我们必须重新运行UT来找到which ones will break(没看懂)
Dependency Injection
跟工厂一样,DI也只是一个设计模式,其核心原则是将行为和解析依赖分离开(separate behaviour from dependency resolution)。在我们的栗子里,RealBillingService不负责寻找TransactionLog和CreditCardProcessor,相反,它们作为构造函数的参数被传递给BillingService
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);
...
}
Dependency Injection with Guice
下面就要介绍到我们的Guice了,DI模式使得代码更加易于测试和可维护,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会检查被注解的构造函数,然后找到每个参数的值。
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());
}
}
}
最后,我们可以把它们放在一起了。
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
...
}
下一章解释了这一切是怎么工作的。