1. Qiita
  2. 投稿
  3. Java

「なぜDI(依存性注入)が必要なのか?」についてGoogleが解説しているページを翻訳した 

  • 279
    ストック
  • 0
    コメント
ストック済み
  • joker1007
  • STAR_ZERO
  • petitviolet
  • pogin503
  • hihira
  • Kuchitama
  • tsumuchan
  • esehara@github
  • a_yasui
  • rynkjm

イマイチ理解しきれていなかったDIに関して調べていところ、Google Guiceの解説がすごく分かりやすかったので、和訳してみました。
(ところどころ意訳気味です。明らかに解釈の誤った訳がありましたら、ご指摘ください)

ちなみにGoogle Guiceというのは、Googleが開発したDIライブラリです。この例ではJavaが使用されていますが、Scalaでも使用可能です。最近Play Frameworkでも採用されたので話題になっているようです。

用語の定義

本文を読む前に目を通すことで、内容をスムーズに理解できます。

用語 意味 本文中の例
サービス 何らかの機能を提供するクラス。 依存される側 CreditCardProcessor、TransactionLog
クライアント サービスを利用するクラス。 依存する側 RealBillingService
依存性解決 サービスとクライアントの依存関係をコード上で明記すること new演算子、Factoryクラス、DI
モック 単体テストにおいて、実際のサービスの代わりに用意する偽のクラス。本物のサービスと同じインターフェースを実装する InMemoryTransactionLog、FakeCreditCardProcessor

本文

DIを行う動機

関係する全てのオブジェクトをひとまとめにするのは、アプリ開発の中でも退屈な作業です。サービス、データ、そしてクライアントを結合する方法には色々あります。それらの方法を比較するために、ピザのネット注文の代金を請求するコードを書いてみましょう。

 public interface BillingService {

  /**
   * クレジットカードに注文代の課金を試みる。
   * 成功しても失敗しても、トランザクションは記録される。
   * @ return トランザクションの領収書(Reciptオブジェクト)を返す。課金が成功した場合、Reciptオブジェクトは正常な値を持つ。失敗した場合、Reciptオブジェクトは課金が失敗した理由を保持する。 
   */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

 実装に際しては、単体テストを書くことにしましょう。テストでは、本物のクレジットカードに課金してしまわないよう、偽のカード課金を表すFakeCreditCardProcessorクラスを使う必要があります。

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

 以下のコードは、カード課金を行うクラス(CreditCardProcessorクラス)と、トランザクションを記録するクラス(TransactionLogクラス)を、愚直に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());
    }
  }
}

 このコードはモジュール性とテスト性に問題があります。本物のクレジット課金を行うクラス(CreditCardProcessorクラス)にコンパイル時点で直接依存しているため、テストをするとカードに課金されてしまいます! また、課金が失敗した時やクレジットサービスが停止している時の挙動をテストするのにも骨が折れます。
  

Factoryクラス

Factoryクラスは、クライアントとサービスの実装とを分離します。単純なFactoryクラスでは、インターフェースを実装したモックをgetterやsetterで操作できます。

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呼び出しをFactoryメソッドに変更するだけです。

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

Factoryを使うと、標準的な単体テストを書くことができます。

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

 このコードもスマートではありません。グローバル変数がモック実装を保持しているため、モックの用意と破棄には細心の注意を要します。tearDownメソッドが失敗すると、グローバル変数はテスト用のインスタンスを参照し続けます。こうなると別のテストに影響が出かねませんし、複数のテストを並行して進めることもできません。

 しかし最大の問題は、依存性がコードの中に隠れてしまっていることです。もし、クレジットカードのなりすましを追跡するクラスを作成し、RealBillingServiceがそのクラスにも依存するようになったとしましょう。テストが失敗した時、依存しているどのクラスに問題があったのかを知るには、テストをもう一回実行しなければなりません。もしFactoryを初期化するのを忘れても、テストを実行するまで気づけません。アプリが肥大化するにつれ、依存性の面倒を見ているFactoryは、生産性を落とす原因になっていきます。
 
 品質問題は、品質保証部や受け入れテストで補足されるでしょう。それで十分かもしれませんが、もっとうまくやることができます。

依存性注入(DI)

 Factoryのように、DIもデザインパターンです。その基本的な考え方は、 振るまいと依存性解決を分離する ことです。この例で言うと、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());
    }
  }
}

もはやFactoryは必要ありません。その上、決まり文句と化した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を受け取ることができます。再度DIパターンを適用すれば、大方問題は解決するかもしれません。しかし最上位のクラスにとっては、フレームワークがあったほうが便利でしょう。さもないと、サービスを使用するのに再帰的に依存関係を構築するはめになります。

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

Guiceによる依存性注入

 DIパターンによってコードのモジュール性とテスト性を上げられますが、Guiceを使えば同じことをもっと簡単に書くことができます。Guiceをカード請求の例に使用するには、まずインターフェースと実装の関係をGuiceに伝える必要があります。この設定は、Moduleインターフェースを実装したJavaクラス(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はアノテーションのついたコンストラクタを調べ、引数の値を見つけてきてくれます。

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

 こうして、全部のクラスをひとまとめにすることができました。Injectorは結合したクラスのインスタンスを取得するのに使います。

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

Getting Startedに、どのようにこのコードが動くのかの説明があります。

編集リクエストを送る投稿者に記事をより良くするための提案ができます 💪
Comments Loading...
あなたもコメントしてみませんか :)
ユーザー登録(無料)
すでにアカウントを持っている方はログイン