モチベーション
アプリ開発において、すべてを一緒に記述していくというのは退屈な仕事である。 データ・サービス・プレゼンテーションのクラスをお互いに接続するには様々なアプローチがある。 これらのアプローチに対して、ピザ注文ウェブサイトのための代金支払いコードを我々ならこう書く。
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を連続して実行するような場合、前のテストが失敗すると後のテストの前提条件が変わってしまうとか、そのような意味)。 さらに、複数のテストを平行して実行することができなくなる(訳注:そういうテストハーネスというのもあるの?)。
しかし、最大の問題は依存性がコードの中に隠れてしまうことである。 もし、CreditCardFraudTrackerという新たな依存を追加した場合、テストを再度実行しておかしなところを見つけなければいけない。 製品においては、チャージが試行されるまでファクトリの初期化を忘れてしまうかもしれない。 アプリが成長するにつれ、このようなファクトリは「漏れ」の原因になる。
Quality problems will be caught by QA or acceptance tests. That may be sufficient, but we can certainly do better.
依存性注入
ファクトリと同様に依存性注入というのは単なるデザインパターンの一つである。 ふるまいと依存解決を分離するというのが基本原理である。 上の例で言えば、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(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()); } }
さて、依存性を追加したり削除したりすると、コンパイラはテスト自体を変更しなければならないことを教えてくれる。 依存性はAPI仕様に現れてくるからだ(訳注:クラス内部にあった依存性がコンストラクタ引数に移動したということ)。
しかし不幸なことに、今度はBillingServiceを使う側が依存性を解決する役目を負わされてしまう。 同じパターンを使って、この問題を解決しなければ! 使う側のコンストラクタがBillingServiceを受け取るようにするのである。 トップレベルのクラスではフレームワークを使うようにしなければ。 さもないと、サービスを使う必要がある場合には、再帰的に依存性を解決をしなければならない (訳注:どんどんと依存性解決の責任がトップレベルの方に押しやられてしまう)。
public static void main(String[] args) { CreditCardProcessor processor = new PaypalCreditCardProcessor(); TransactionLog transactionLog = new DatabaseTransactionLog(); BillingService billingService = new RealBillingService(creditCardProcessor, transactionLog); ... }
Guiceによる依存性注入
依存性注入パターンはコードをモジュール化し、テスト可能にする。 Guiceはそれを簡単に行うことができる。 今回の例にGuiceを使うためには、まずインターフェースをどのように実装にマップするかを指定しなければならない。 このコンフィグレーションはGuice-Moduleの中で行われる。Guice-Moduleは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のコンストラクタに、Guiceの目印である@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(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()); } } }
最後に、すべてをまとめる。 バインドされたクラスであれば、インジェクタを使ってそのインスタンスを取得することができる。
public static void main(String[] args) { Injector injector = Guice.createInjector(new BillingModule()); BillingService billingService = injector.getInstance(BillingService.class); ... }
始め方にて、どうやってこれを行うかを説明する。