You are not allowed to do newaccount on this page. Login and try again.

Clear message
Locked History Actions

guice/ServiceLocator

Injectorをサービスロケータとして使う

DIの考え方は非常にとっつきにくい。とっつきにくい原因は「制御の逆転」である。通常、我々があるクラスを作成する場合はその中で「好きな時に好きなオブジェクトを好きなだけ生成して使う」というやり方である。が、DIではそうはならない。「そのクラスで使うべきであろうオブジェクトがどこかで勝手に生成され、勝手に用意される」といった具合になる。

つまりこれは、考えるべき問題の範囲がクラスの外側にはみ出してしまうことを意味する。これまではクラスの中でだけ問題(いつどんなオブジェクトを生成するか)を考えればよかったにも関わらず、DIを使うとクラス以外の部分でそれらを検討する必要が出てくる。これは面倒だし、間違いが発生する可能性も大きい。追うべき範囲が広がるのでおそらくデバッグも難しくなる。

SpringのようにオブジェクトのセットアップをXMLを使って行うフレームワークの場合はさらに深刻と思われる。XMLの記述とJavaの記述が完全に協調していなくては、正しく動作しないプログラムとなってしまうのはもちろんだが、それらの二つの言語の間でつじつまが合っているかを検証する手段はない(IDEのプラグインなどでXML上に指定されたクラスが実際に存在するかどうかを検証するものは存在するらしい)。

我々がプログラミングを行うとき鉄則とすべきものは、考慮すべき範囲(スコープ)を極力小さくするということである。例えば、変数やメソッドの有効範囲をprivate, protected, public等とスコープ分けすることにより、それらを分割して統治することができるのである。ところが、DIでは、これとは逆のことが起こる。「クラスの事情」がそのクラスの枠を飛び越えてしまう。

問題範囲をクラスの中に閉じ込めておくためには、DIよりもサービスロケータが適当である。サービスロケータの使用であれば、我々は「好きなときに好きなオブジェクトを好きなだけ生成して使う」ということができる。サービスロケータの唯一の欠点は、プログラムがロケータに依存してしまうという点だけである(たぶん)。

しかし、「依存する」ということをそんなに気にする必要があるだろうか?Springの場合はともかく、Guiceの場合はGuiceというフレームワークの存在に依存せずに利用することはできない。少なくとも、@Inject等のアノテーションは使わざるを得ないからである。

これに対して、自らのアプリが自ら提供したサービスロケータに依存することがそれほど問題だろうか?アプリの一部をライブラリとして他に提供するというのであれば、そのライブラリ専用のサービスロケータを作成すればよい話である。

しかし、アプリケーション全体に渡って全面的に独自のサービスロケータを使用する場合、その構成・設定を行うのは非常に手間のかかる作業である。これに対して、GuiceのInjectorをサービスロケータとして利用すれば非常に便利である。以下ではこの利用法を考察してみる。

Injectorはサービスロケータである

GuiceのInjectorはサービスロケータである。

Injector injector = Guice.createInjector(...);
UserLookup userLookup = injector.getInstance(UserLookup.class);

などとすることにより、Injectorは自動的にUserLookupインターフェースを実装したクラスのオブジェクトを返してくる。実装クラスとしてどのようなオブジェクトを返すかは、Injector作成時に与えられたモジュールによって決定される。

これはサービスロケータそのものの動作である。それでは次に、このサービスロケータをどのように保持して利用するかを考えてみる。

グローバルなシングルトンとして利用する

単純に書けば次のようなことである。

public class InjectorHolder {
  public static Injector injector;
}
....
 Injector injector = Guice.createInjector(...);
 InjectorHolder.injector = injector;

Injectorを生成したら、すべてのクラスから参照できる場所にそれを放り込んでおく、各クラスはそれを使ってオブジェクトを生成する。あるいは、ライブラリとして分割するのであれば、そのライブラリごとにシングルトンを分けてもよい。

// Aライブラリパッケージにて
public class InjectorHolderA {
  public static Injector injector;
}

// Bライブラリパッケージにて
public class InjectorHolderB {
  public static Injector injector;
}

Injectorを生成したら、これらのすべてのシングルトンにInjectorを代入してまわる必要がある。

これでも全く構わないと思われるが、 このようなシングルトンの使い方に抵抗を示す方も多い。であれば、次のような方法はどうか?

Injectorをinjectする

もともとGuiceでは、ProviderやFactoryProviderで作成されたファクトリをInjectする機能がある。なんのことはない、これは「一つのクラスについてだけのサービスロケータ」であると考えられる。これが可で「複数の任意のクラスについてのサービスロケータを注入すること」が不可である理由はどこにも無い。

つまり、Providerやファクトリをinjectするのではなく、Injectorそれ自体をinjectすればよいのである。

public class UserLookupImpl implements UserLookup {
  Injector injector;
  @Inject
  public UserLookupImpl(Injector injector) {
   this.injector = injector;
  }
}

しかし、これを行うにはInjector生成を次のように行わなければならない。

    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override protected void configure() {
        bind(Injector.class).toInstance(injector);
      }
    });

が、これは不可能である。また、生成済みのInjectorの中にModuleを追加するということもできないらしい。

※上は間違い。作成されるInjectorのモジュール中にInjector.classに対するバインディングがなくてもそのInjector自体が注入される。したがって、特に何もしなくても、

public class UserLookupImpl implements UserLookup {
  Injector injector;
  @Inject
  public UserLookupImpl(Injector injector) {
   this.injector = injector;
  }
}

と書けば、そのUserLookupImplを生成したInjector自体が注入される。

ビルトインバインディングを参照のこと。

InjectorHolderをinjectする

次善の策として、InjectorをラップしたInjectorHolderをinjectするという方法が考えられる。

import com.google.inject.*;
public class InjectorHolder {
  private Injector injector;  
  public void setInjector(Injector injector) {
    assert this.injector == null && injector != null;
    this.injector = injector;
  }
  public <T> T getInstance(Class<T>type) {
    return injector.getInstance(type);
  }
}

としておき、

    final InjectorHolder holder = new InjectorHolder();
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override protected void configure() {
        bind(InjectorHolder.class).toInstance(holder);
      }
    });
    holder.setInjector(injector);

とする。

InjectorHolderの利用例

以下に実際の例をあげる。

@ImplementedBy(UserLookupImpl.class)
public interface UserLookup {
  public UserInfo lookup(String userId, String password);
}
...
public class UserLookupImpl implements UserLookup {    
  @Inject InjectorHolder holder;  
  public UserInfo lookup(String user, String password) {
    UserInfo info = holder.getInstance(UserInfo.class);
    info.setUser(user);
    return info;
  }
}
....
@ImplementedBy(UserInfoImpl.class)
public interface UserInfo {  
  public void setUser(String user);
}
....
public class UserInfoImpl implements UserInfo {
  private String user;
  public UserInfoImpl() {
  }
  public void setUser(String user) {
    this.user = user;
  }
  @Override public String toString() {
    return "UserInfoImpl:" + user;
  }
}
...
public class Main {
  public static void main(String[]args) {
    final InjectorHolder holder = new InjectorHolder();
    Injector injector = Guice.createInjector(new AbstractModule() {
      @Override protected void configure() {
        bind(InjectorHolder.class).toInstance(holder);
      }
    });
    holder.setInjector(injector);
    
    UserLookup userLookup = injector.getInstance(UserLookup.class);    
    UserInfo info = userLookup.lookup("guest", "guest");    
    System.out.println(info.toString());
  }
}

注目すべきはUserLookupImplクラスである。このインスタンスの作成時に注入されるオブジェクトはInjectorHolderただ一つである。lookupが呼び出されると、そのユーザのためのUserInfoInjectorHolderを使って作成する。

これは、以前に書いた例「ProviderとFactoryProvider」とほとんど変わることがない。単純に、「Provider<UserInfo>userInfoProvider」という「UserInfoのみを生成するオブジェクト」がInjectorHolderという「何でも生成可能オブジェクト」に変更されただけである。

まとめ

Guiceの機能をそのまま素直に使おうと思うと、あるクラスでは以下のような状態になってしまうかもしれない。

class A {
  // 必ず使うのでAの生成時に生成されているべきオブジェクト
  @Inject B b;

  // 使うか使わないかわからないが、とりあえず生成しておく場合
  @Inject C c;

  // 使うか使わないかわからないので、必要なときに生成できる仕組み
  @Inject Provider<D> dProvider;
  D d;
  D getD() {
    if (d == null) d = dProvider.get();
    return d;
  }

  // いくつのオブジェクトが必要になるかわからないのでProviderだけ取得しておくもの1
  @Inject Provider<E> eProvider;

  // いくつのオブジェクトが必要になるかわからないのでProviderだけ取得しておくもの2
  @Inject Provider<F> fProvider;
}

このように、あまりに冗長になることは明らかである。InjectorHolderを利用すれば上述の@Injectはすべて必要なくなる。

class A {
  InjectorHolder holder;
  B b;
  C c;
  D d;
  @Inject
  A(InjectorHolder holder) {
    this.holder = holder;
    b = holder.getInstance(B.class);
    c = holder.getInstance(C.class);  
  }
  D getD() {
    if (d == null) d = holder.getInstance(D.class);
    return d;
  }
}

参考資料

上の「まとめ」より

近ごろ急増している軽量コンテナのすべてが、共通して基礎としているパターンは、どのようにしてサービスの組み立てを行うかについてのパターン――Dependency Injection パターンである。 Dependency Injection は Service Locator の代替案として有効である。アプリケーションクラスを構築するにあたっては、両者ともだいたい同じようなものだが、私は Service Locator のほうが少し優勢だと思う。こちらのほうが振る舞いが素直だからだ。しかしながら、構築したクラスが複数のアプリケーションで利用されるのであれば、 Dependency Injection のほうがより良い選択肢となる。

Service Locator と Dependency Injection とのどちらを選ぶかは大した問題ではない。サービスの設定をアプリケーション内でのサービス利用から分離するという原則こそが重要なのだ。