在应用中组装各个封装好的类,有时候是一件很乏味的事情。有几种办法可以把数据层、业务层、表现层的代码整合在一起。下面通过一个在线披萨下订单的业务来对比这几种实现方法。
// 定义下订单接口 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 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); ... } 复制代码
原文链接: www.wetimer.com/wei-shi-yao…