原文链接

动机

将所有的内容连接在一起时应用开发的一个单调乏味的部分。有几种方式来将数据、服务、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. java有关 String char 常见问题 编辑中

    1 输入输出有关 Scanner 的next()方法 返回值是String 所以尝试获得char时 应该用input.next().charAt[0] 2 空值 String 中null是指 对象引用 ...

  2. 基于mysql的一些sql语法

    Sql: distinct: select distinct * from tea; Tea中每行的数据必有不同,若有两行相同的,则只输出一行 Limit: select * from stu lim ...

  3. VUE的一个数据绑定与页面刷新相关的bug

    1.场景: N层嵌套的循环查询业务场景,框架是vue.其中在最后一层查完之后,还需要查其中每一项的两个属性,类型都是列表.查完之后将其赋值给一个变量用于页面展示.代码如下: (1)异常代码: getS ...

  4. 为什么区块链和加密行业需要 Web 3?

    为什么区块链和加密行业需要 Web 3? “由于人们都想从互联网上获得好处,互联网已经演变成了一个导致不公平和分裂的引擎,它被强大的力量所支配,并且任由其摆布.“——万维网之父.互联网先驱 Tim B ...

  5. Azure存储账户的日志分析方法

    1.首先确认日志功能是否开启(日志文件根据存储账户的类型,按使用量收费 . 2.在存储账户-Usage(classic)-Metrics中查看突出流量的时间: 3.在Explorer中下载对应时间点的 ...

  6. Python--day11(函数的参数)

    今日主要内容 1.  函数的参数 2.  函数的嵌套调用 1.  形参与实参 1.  参数介绍: 函数为什么要有参数:因为内部的函数体需要外部的数据 怎样定义函数的参数:在定义函数阶段,函数名在后面( ...

  7. centos7之openvpn搭建

    一.环境介绍 操作系统centos7.4 openvpn版本:openvpn-2.1 lzo版本:lzo-2.03 二.搭建 关闭firewalld防火墙,并设置开机不启动.关闭selinux sys ...

  8. JAVA反射优化

    ****************** 转自 https://my.oschina.net/19921228/blog/3042643 *********************** 比较反射与正常实例 ...

  9. this.setData , that.setData , this.data.val三者之间的区别和作用

    1.this.setData({ }) <view bindtouchmove="tap_drag" bindtouchend="tap_end" bin ...

  10. odoo 权限问题

    odoo 权限问题 权限组问题 权限组是为了将人员按组划分同一分配权限.权限组的建立是基于每个应用来实现的 建立一个应用的分组(可省略,主要用于创建用户时有选择项) 建立一条record记录model ...