使用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
并不负责查找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());
}
}
如果我们在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);
}
}
我们通过添加@Inject
到RealBillingService
的构造方法,让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);
...
}