Locked History Actions

Diff for "guice/Manual/UserGuide/Motivation"

Differences between revisions 1 and 2
Deletions are marked like this. Additions are marked like this.
Line 132: Line 132:

This code is clumsy. A global variable holds the implementations, so we need to be careful about setting it up and tearing it down. Should the tearDown fail, the global variable continues to point at our test instance. This could cause problems for other tests. It also prevents us from running multiple tests in parallel.

But the biggest problem is that the dependencies are hidden in the code. If we add a dependency on a CreditCardFraudTracker, we have to re-run the tests to find out which ones will break. Should we forget to initialize a factory for a production service, we don't find out until a charge is attempted. As the application grows, babysitting factories becomes a growing drain on productivity.
このコードはぎこちない。グローバル変数が実装を保持しているので、セットアップとティアダウンには注意しなければならない(訳注:テストに使うのであれば、テスト前にセットし、テスト後には以前のものを復帰するとか、そのような意味)。
ティアダウンに失敗すると、グローバル変数はテスト用のインスタンスを保持し続けてしまう。
これによって、他のテストに問題が発生しかねない(訳注:例えばJUnitを連続して実行するような場合、前のテストが失敗すると後のテストの前提条件が変わってしまうとか、そのような意味)。
さらに、複数のテストを平行して実行することができなくなる(訳注:そういうテストハーネスというのもあるの?)。


But the biggest problem is that the dependencies are hidden in the code. If we add a dependency on a CreditCardFraudTracker, we have to re-run the tests to find out which ones will break.
Should we forget to initialize a factory for a production service, we don't find out until a charge is attempted. As the application grows, babysitting factories becomes a growing drain on productivity.
Line 141: Line 144:
Like the factory, dependency injection is just a design pattern. The core principal is to separate behaviour from dependency resolution. In our example, the RealBillingService is not responsible for looking up the TransactionLog and CreditCardProcessor. Instead, they're passed in as constructor parameters:
ファクトリと同様に依存性注入というのは単なるデザインパターンの一つである。
ふるまいと依存解決を分離するというのが基本原理である。
上の例で言えば、RealBillingServiceは、TransactionLogとCreditCardProcessorを取得する責任を負わされてはいないということ。
そうではなく、コンストラクタのパラメータとして渡されるのである。

{{{
Line 167: Line 174:

We don't need any factories, and we can simplify the testcase by removing the setUp and tearDown boilerplate:
}}}

ファクトリは必要無いし、かつsetUpやtearDownのようなボイラープレートを削除できてテストケースがシンプルになる。

{{{
Line 190: Line 199:

Now, whenever we add or remove dependencies, the compiler will remind us what tests need to be fixed. The dependency is exposed in the API signature.
}}}

さて、依存性を追加したり削除したりすると、コンパイラはテスト自体を変更しなければならないことを教えてくれる。
The dependency is exposed in the API signature.
Line 195: Line 206:
{{{
Line 202: Line 214:

Dependency Injection with Guice
}}}

== Guiceによる依存性注入 ==
Line 206: Line 219:
{{{
Line 215: Line 228:
}}}
Line 217: Line 230:
{{{
Line 243: Line 256:
}}}
Line 245: Line 258:
{{{
Line 251: Line 264:

Getting started
explains how this all works.
}}}


[[guice/Manual/UserGuide/GettingStarted|始め方]]にて、どうやってこれを行うかを説明する。

モチベーション

アプリ開発において、すべてを一緒に記述していくというのは退屈な仕事である。 データ・サービス・プレゼンテーションのクラスをお互いに接続するには様々なアプローチがある。 これらのアプローチに対して、ピザ注文ウェブサイトのための代金支払いコードを我々ならこう書く。

public interface BillingService {

  /**
   * クレジットカードにチャージする。成功・失敗のいずれの場合にも記録される。
   *
   * @return トランザクションのレシートを返す。チャージ成功の場合は成功レシートである。
   *  そうでなければ、レシートには失敗の理由がしめされる。
   */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

実装はもちろんだが、ユニットテストも書くことになる。テストではFakeCreditCardProcessorが必要である。 なぜなら、実際のクレジットカードにチャージしてはならないから!

直接コンストラクタ呼び出し

クレジットカードプロセッサとトランザクションロガーを今書いたとしよう。

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(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 creditCardProcessor) {
    instance = creditCardProcessor;
  }

  public static CreditCardProcessor getInstance() {
    if (instance == null) {
      throw new IllegalStateException("CreditCardProcessorFactory not initialized. "
          + "Did you forget to call CreditCardProcessor.setInstance() ?");
    }
    
    return instance;
  }
}

クライアントコードでは単純に、コンストラクタ呼び出しをファクトリルックアップに変更するとしよう。

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(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 creditCardProcessor = new FakeCreditCardProcessor();

  @Override public void setUp() {
    TransactionLogFactory.setInstance(transactionLog);
    CreditCardProcessorFactory.setInstance(creditCardProcessor);
  }

  @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, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100, creditCardProcessor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

このコードはぎこちない。グローバル変数が実装を保持しているので、セットアップとティアダウンには注意しなければならない(訳注:テストに使うのであれば、テスト前にセットし、テスト後には以前のものを復帰するとか、そのような意味)。 ティアダウンに失敗すると、グローバル変数はテスト用のインスタンスを保持し続けてしまう。 これによって、他のテストに問題が発生しかねない(訳注:例えばJUnitを連続して実行するような場合、前のテストが失敗すると後のテストの前提条件が変わってしまうとか、そのような意味)。 さらに、複数のテストを平行して実行することができなくなる(訳注:そういうテストハーネスというのもあるの?)。

But the biggest problem is that the dependencies are hidden in the code. If we add a dependency on a CreditCardFraudTracker, we have to re-run the tests to find out which ones will break. Should we forget to initialize a factory for a production service, we don't find out until a charge is attempted. As the application grows, babysitting factories becomes a growing drain on productivity.

Quality problems will be caught by QA or acceptance tests. That may be sufficient, but we can certainly do better.

依存性注入

ファクトリと同様に依存性注入というのは単なるデザインパターンの一つである。 ふるまいと依存解決を分離するというのが基本原理である。 上の例で言えば、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(order.getAmount(), creditCard);
      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 creditCardProcessor = new FakeCreditCardProcessor();

  public void testSuccessfulCharge() {
    RealBillingService billingService
        = new RealBillingService(creditCardProcessor, transactionLog);
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, creditCardProcessor.getCardOfOnlyCharge());
    assertEquals(100, creditCardProcessor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

さて、依存性を追加したり削除したりすると、コンパイラはテスト自体を変更しなければならないことを教えてくれる。 The dependency is exposed in the API signature.

Unfortunately, now the clients of BillingService need to lookup its dependencies. We can fix some of these by applying the pattern again! Classes that depend on it can accept a BillingService in their constructor. For top-level classes, it's useful to have a framework. Otherwise you'll need to construct dependencies recursively when you need to use a service:

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

Guiceによる依存性注入

The dependency injection pattern leads to code that's modular and testable, and Guice makes it easy to write. To use Guice in our billing example, we first need to tell it how to map our interfaces to their implementations. This configuration is done in a Guice module, which is any Java class that implements the Module interface:

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

We add @Inject to RealBillingService's constructor, which directs Guice to use it. Guice will inspect the annotated constructor, and lookup values for each of parameter.

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(order.getAmount(), creditCard);
      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());
    }
  }
}

Finally, we can put it all together. The Injector can be used to get an instance of any of the bound classes.

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new BillingModule());
    BillingService billingService = injector.getInstance(BillingService.class);
    ...
  }

始め方にて、どうやってこれを行うかを説明する。