原文链接

动机

将所有的内容连接在一起时应用开发的一个单调乏味的部分。有几种方式来将数据、服务、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没有责任查找TransactionCreditCardProcessor。相反,它们作为构造函数参数传入:

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());
}
}
}

我们不需要任何的工厂,而且我们可以通过去除setUptearDown样板代码来简化我们的测试用例:

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解释了这是怎么工作的。

【翻译】 Guice 动机——依赖注入的动机的更多相关文章

  1. 依赖注入与Unity(一) 介绍

        在你学习依赖注入和Unity之前,你需要明白你为什么要使用它们.为了明白为什么要使用它们,你应该明白依赖注入和Unity能够帮助你解决什么类型的问题.作为介绍部分,这一章不会涉及太多关于Uni ...

  2. ABP(现代ASP.NET样板开发框架)系列之6、ABP依赖注入

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之6.ABP依赖注入 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)” ...

  3. ABP依赖注入

    ABP依赖注入 点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之6.ABP依赖注入 ABP是“ASP.NET Boilerplate Project (ASP.N ...

  4. Spring之 IOC&依赖注入

    0x01.Spring 1什么是Spring ​ Spring 是一个开源框架,是为了解决企业应用程序开发复杂性而创建的(解耦). ​ 框架的主要优势之一就是其分层架构,分层架构允许您选择使用哪一个组 ...

  5. [Android]使用Dagger 2依赖注入 - DI介绍(翻译)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5092083.html 使用Dagger 2依赖注入 - DI介 ...

  6. [Android]使用Dagger 2进行依赖注入 - Producers(翻译)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/6234811.html 使用Dagger 2进行依赖注入 - P ...

  7. [Android]使用Dagger 2依赖注入 - API(翻译)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5092525.html 使用Dagger 2依赖注入 - API ...

  8. [Android]使用Dagger 2依赖注入 - 自定义Scope(翻译)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5095426.html 使用Dagger 2依赖注入 - 自定义 ...

  9. [Android]使用Dagger 2依赖注入 - 图表创建的性能(翻译)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5098943.html 使用Dagger 2依赖注入 - 图表创 ...

随机推荐

  1. LeetCode算法题-Reach a Number(Java实现)

    这是悦乐书的第310次更新,第331篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第179题(顺位题号是754).你站在无限数字线的0号位置.在目的地有个target.在 ...

  2. slice 与 splice 的区别

    slice: 定义一个数组:let b = ['a','b','c','d','e'] b:["a", "b", "c", "d& ...

  3. 借助FreeHttp任意篡改http报文 (使用·实现)

    引言 FreeHttp是一个Fiddler插件借助FreeHttp您可按照您自己的设定修改请求或响应报文,这对测试及调试都非常有用 比如您发现线上页面js文件错误,直接使用规则替换新的js文件您可以在 ...

  4. SQLserver 获取当前时间

    1. 获取当前日期 select GETDATE() 格式化: select CONVERT(varchar,GETDATE(),120) --2017-05-12 16:33:10 2. 获取当前年 ...

  5. mysql查询order by 指定字段排序

    当MySQL查询时排序的字段不是数字时而是汉字的时候也可以用when  then 来指定排序. 列如yewu_check表的status 字段不是0,1,2而是汉字待办,已办,退回.可以如下写法: S ...

  6. Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用(转)

    原文地址:https://www.cnblogs.com/fashflying/p/6908028.html 从3.1开始,Spring引入了对Cache的支持.其使用方法和原理都类似于Spring对 ...

  7. PHP九大接口视频教程( 支付宝,QQ,短信接口,微信接口开发, 支付宝即时到账接口开发三级分销全套)

    PHP九大接口视频教程(  支付宝,QQ,短信接口,微信接口开发, 支付宝即时到账接口开发三级分销全套) 需要的联系我:QQ: 1844912514 PHP九大接口视频教程(  支付宝,QQ,短信接口 ...

  8. SpringBoot+Swagger整合API

    SpringBoot+Swagger整合API Swagger:整合规范的api,有界面的操作,测试 1.在pom.xml加入swagger依赖 <!--整合Swagger2配置类--> ...

  9. Module build failed: Error: Cannot find module 'babel-runtime/core-js/get-it

    npm i babel-loader@7.1.5 -D

  10. IP地址、子网掩码、默认网关是什么意思?

    (一)  问题解析 001.   问:  IP地址,子网掩码,默认网关,DNS服务器,有什么区别呀?我知道没有IP地址就不能上网,我也知道没设DNS就不能上外网,可它们都有什么功能,有什么区别呢?还有 ...