Google Guice(一) 初识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);
}

这个接口只有一个方法,支付订单。

然后为了测试这个接口,我们需要实现它。下面通过不同的方法实现这一功能。

直接调用构造器

这里我们是直接创建(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());
    }
  }
}

这段代码就暴露了一些对于模块化和单测不友好的问题。这个方法是依赖一个真实的PaypalCreditCardProcessor(真的能刷你钱的那种)。那么在编写这个测试类的时候,开发人员如果需要测试这个功能的话,就必须要用真实的信用卡来支付,否则没法判断服务是否可用。

工厂

工厂类可以让客户端和实现类解耦。一般的工厂都会通过一个静态方法来获取和设置接口的实现类。工厂方法和工厂类,不仅仅可以让某些繁琐的对象创建过程变得可复用,更是使对象创建的过程有了更多的可能性。下面是一个简单工厂的样例代码:

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

可以看到,上述代码中,工厂方法并不依赖于具体的CreditCardProcessor类,即没有创建一个实例。而客户端由可以通过setInstance方法来传递这个对象。对原有的功能没有影响,却可以大大地扩展程序的灵活性。

接下来改写客户端代码 ,让它没有new的操作,这样即使没有创建真正的CreditCardProcessor对象,工厂也会返回一个自己创建的用于测试的CreditCardProcessor类型的对象回来。

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

但是,这会有另外一个问题。以为工厂类中需要返回的是一个全局的静态变量。也就以为在我们的测试类中需要在setUp方法中给他赋值,然后在tearDown方法中将这些变量注销掉。如果因为某些原因导致这个tearDown方法没有执行,那么这个全局的变量就不会被注销,这样在并行执行测试类的方法时就有可能让其他的测试类没法通过。

而且,更大的问题是类之间的依赖关系被隐藏在了工厂代码之中,如果我们添加了新的依赖,则必须要重跑测试了。在这一过程中,我们要是没有初始化工厂的话,则代码只有运行到这一步的时候,我们才会知道。随着我们的业务的扩大,这种压力也会越来越大。

代码质量问题,可以被质量监管或者完善的测试更正。但是我们可以做的更好!

我们稍微暂停一下整理整理思路。从一开始对象在方法中创建,到后面对象在工厂中创建,通过工厂和静态变量传递到方法中。我们一直改变的只是对象的获取途径。在这一前提下,我们是否可以考虑更多的可能呢?

依赖注入

同工厂相似,依赖注入也是一种设计模式。其核心的原则是将创建行为从依赖解决中分离出来。在下面的例子中,RealBillingService并不负责TransactionLogCreditCardProcessor对象的创建。他们是作为构造器参数被传递进来的:

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

这样我们即使不需要过工厂,也能够自己创建需要processor来完成支付的动作了。

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

现在,如果我们移除依赖的话,代码是不会通过编译的。依赖在接口的签名中就已经暴露出来了。 这一种方式就是依赖注入了,类依赖的对象实例在需要的时候通过构造器或者其他的方法注入进来。

但是不尽如人意的是,现在BillingService的客户端需要自己管理这些依赖了。当然,我们可以再使用模式来解决这个问题。依赖于这些的类,可以把BllingService作为一个参数传递到它们的构造器之中。此时,如果没有一个好的框架,我们最终还是要在顶层创建这个类,然后一层一层地往下传递,注入。

public static void main(String[] args) {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();
    BillingService billingService
        = new RealBillingService(processor, transactionLog);
    ...
  }

使用Guice完成依赖注入

Google的Guice框架可以简单的完成代码中的依赖注入,而且,通过这个框架写出的代码可以实现模块化和测试。在我们的支付demo中使用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);
  }
}

上面通过模块配置,将接口和实现类绑定在了一起。然后在RealBillingService的构造器上加上@Inject注解,这样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);
    ...
  }

如何实现的?

在上面的这种方式中,对象通过注入接收了在构造器中的对象。为了构造一个对象,我们首先要确定的就是它所依赖对象的绑定关系,即他需要的是什么对象。要知道一个接口的实现类可能有很多很多个。但是如果要绑定一个依赖的话,我们可能还需要绑定这些依赖的依赖,这样一直下去,我们就需要维护一个对象依赖图了。

人工创建一个对象依赖图是相当费劲的,而且容易出错,导致测试困难。当然,如果使用Guice的话,这个对象依赖图就不需要你来创建了,它会帮你做。但是首先我们得配置Guice,让它按照我们所希望的方式来构建对象依赖图。

举个例子,在BillingService 类中,我们依赖 CreditCardProcessorTransactionLog。 为了让这个类的构造器能够被Guice调用,我们需要使用@Inject注解:

class BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  @Inject
  BillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    ...
  }
}

这个注解就告诉了Guice,构造器依赖PaypalCreditCardProcessorDatabaseTransactionLog 对象需要注入进来。之后,Guice就会通过绑定关系来注入他们的实现类。Module就是一个绑定关系的集合。其绑定的方式通过方法调用很容易理解,就跟自然语言的表达方式类似:

public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {

     /*
      * This tells Guice that whenever it sees a dependency on a TransactionLog,
      * it should satisfy the dependency using a DatabaseTransactionLog.
      */
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);

     /*
      * Similarly, this binding tells Guice that when CreditCardProcessor is used in
      * a dependency, that should be satisfied with a PaypalCreditCardProcessor.
      */
    bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
  }
}

不同的moduleinjector(injector)指定了不同的绑定关系。在获取对象的时候,我们需要先创建一个injecotr,之后我们就可以使用它来获取我们想要的对象了。

 public static void main(String[] args) {
    /*
     * Guice.createInjector() takes your Modules, and returns a new Injector
     * instance. Most applications will call this method exactly once, in their
     * main() method.
     */
    Injector injector = Guice.createInjector(new BillingModule());

    /*
     * Now that we've got the injector, we can build objects.
     */
    BillingService billingService = injector.getInstance(BillingService.class);
    ...
  }

通过上面的demo,我们已经创建了一个小小的对象依赖图。这个图包括了BillingService和它所依赖的CreditCardProcessor,TransactionLog

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 2.1 我们的理念是:让别人为你服务 IoC是随着近年来轻量级容器(Lightweight Container)的...
    好好学习Sun阅读 2,626评论 0 11
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,288评论 0 4
  • Guice是谷歌推出的一个轻量级依赖注入框架,帮助我们解决Java项目中的依赖注入问题。如果使用过Spring的话...
    乐百川阅读 25,977评论 6 25
  • “我不去想,是否能够成功 ,既然选择了远方 ,便只顾风雨兼程。我不去想,能否赢得爱情 ,既然钟情于玫瑰 ,就勇敢地...
    心雨_c91e阅读 605评论 2 6