Hatena::ブログ(Diary)

nodchipの日記 このページをアンテナに追加 RSSフィード Twitter

2013-01-26

心地良すぎるDependency Injectionライブラリ Guice

etc9さんの"心地良すぎるモックライブラリ Mockito"がとても勉強になったので、似たような形式でGuiceの紹介をしてみます。

Dependency Injection

きちんと勉強したわけではないので間違っていたらごめんなさい。Dependency Injection (DI)はユニットテストを書きやすくするためのクラスの書き方の一つです。クラスAが内部でクラスBを使うとき、Aの中でBをnewする代わりに、AのコンストラクタやsetterでBのインスタンスを外部から渡せるしておきます。こうしておくとAのユニットテストを書くとき、Bの動作を真似るモックオブジェクトを渡すことで、Bの中身を考えずにAをのテストを書けるようになります。これはAとBの動作を同時に考えてテストを書くよりずっと楽だと思います。依存性注入とかレポジトリパターンとも呼ばれているっぽいです。

GuiceはDIの補助をしてくれるライブラリです。Guice Moduleと呼ばれるクラスにインスタンス生成ルールを記述しておくと、依存関係を解析した上でインスタンスを作成してくれます。Guiceの内部ではリフレクションで依存クラスの解析とインスタンスの作成をしているんだと思います。多分。単にインスタンスを作成するだけでなく、Singletonにしてくれたり、Factoryクラスを自動実装してくれたり、色々やってくれます。

簡単な例

以下の例ではInjectorを作成したあと、Injector経由でクラスAのインスタンスを作成しています。デフォルトのルールでインスタンス生成ができるので、Moduleは使っていません。

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Sample00 {
  public static class A {
    public void run() {
      System.out.println("A#run() is called.");
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector();
    A a = injector.getInstance(A.class);
    a.run();
  }
}

出力

A#run() is called.

Aのインスタンスを作るよう指示しました。するとinjectorが内部でインスタンスを作って返してくれます。

引数なしコンストラクタ

引数なしのコンストラクタは特別な指定なしで呼び出してくれます。

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Sample01 {
  public static class A {
    public A() {
      System.out.println("A() is called.");
    }

    public void run() {
      System.out.println("A#run() is called.");
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector();
    A a = injector.getInstance(A.class);
    a.run();
  }
}

出力

A() is called.
A#run() is called.

Aのインスタンスを作るよう指示しました。するとinjectorはAの引数なしコンストラクタを呼び出してインスタンスを作ってくれます。

Constructor Bindings

コンストラクタ引数を持つ場合、引数の型に応じたインスタンスを生成して渡した上で、対象のインスタンスを生成してくれます。ただし、引数付きのコンストラクタには@Injectアノテーションを付けておかないとエラーになってしまいます。クラス間の依存関係がありますが、まだModuleは使わなくて大丈夫です。

バグ防止のためguavaライブラリのPreconditions.checkNotNull()でnullチェックをしています。

import com.google.common.base.Preconditions;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;

public class Sample02 {
  public static class A {
    public A() {
      System.out.println("A() is called.");
    }

    public void run() {
      System.out.println("A#run() is called.");
    }
  }

  public static class B {
    private final A a;

    @Inject
    public B(A a) {
      this.a = Preconditions.checkNotNull(a);
      System.out.println("B() is called.");
    }

    public void run() {
      a.run();
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector();
    B b = injector.getInstance(B.class);
    b.run();
  }
}

出力

A() is called.
B() is called.
A#run() is called.

Bのインスタンスを作るよう指示しました。するとinjectorはBのコンストラクタ引数にAが入っているのを見つけ、先にAのインスタンスを作ります。そしてそれをBのコンストラクタに渡して、Bのインスタンスを作ってくれます。出力からBにAのインスタンスが渡されているのが分かります。

ちなみに@Injectを付けないとこんなエラーが出ます。

Exception in thread "main" com.google.inject.ConfigurationException: Guice configuration errors:

1) Could not find a suitable constructor in Sample02$B. Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private.
  at Sample02$B.class(Sample02.java:20)
  while locating Sample02$B

1 error
  at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:1004)
  at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:961)
  at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1013)
  at Sample02.main(Sample02.java:32)

こういったエラーがコンパイル時に分からないのがGuiceの難点の一つです。どうにかならないのでしょうか・・・。

Constructor Bindingsの他にもsetterによるbindingやfield bindingといった方法もありますが、Constructor Bindings推奨だったように思います。

Linked Bindings

Module初登場です。Moduleを使って、Aをinjectする代わりにAを継承したBをinjectするよう指定します。これにはbind()メソッドを使います。"bind(A.class).to(B.class);"は、"型Aの引数にはBのインスタンスを結び付けろ"という意味です。ModuleはcreateInjector()の引数に渡します。

Moduleのconfigure()メソッドの中ではinstall()というメソッドも使えます。これは別のModuleの定義を取り込むというものです。別のパッケージで定義されているModuleの内容を取り込む特に使うと良いでしょう。

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class Sample03 {
  public interface A {
    void run();
  }

  public static class B implements A {
    public B() {
      System.out.println("B() is called.");
    }

    public void run() {
      System.out.println("B#run() is called.");
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(A.class).to(B.class);
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    A a = injector.getInstance(A.class);
    a.run();
  }
}

出力

B() is called.
B#run() is called.

Aのインスタンスを作るよう指示しました。injectorはModuleの中に"型Aの引数にはBのインスタンスを結び付けろ"と指示があるので、Bのインスタンスを作って返してくれます。

Aがクラスだったり抽象クラスでも動きます。Aが別のクラスの引数に指定されていても動きます。

Scopes

即別な指定をしない場合、Guiceインスタンスをinjectのたびに新しいインスタンスを生成します。ModuleでScopes.SINGLETONを指定すると、インスタンスをinjectするときに最初の一回だけインスタンスを作ってinjector内で使いまわすようになります。いわゆるSingletonパターン的な使い方ができます。

import com.google.common.base.Preconditions;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Scopes;

public class Sample04 {
  public static class A {
    public A() {
      System.out.println("A() is called.");
    }

    public void run() {
      System.out.println("A#run() is called.");
    }
  }

  public static class B {
    private final A a;

    @Inject
    public B(A a) {
      this.a = Preconditions.checkNotNull(a);
      System.out.println("B() is called.");
    }

    public void run() {
      System.out.println("B#run() is called.");
      a.run();
    }
  }

  public static class C {
    private final A a;

    @Inject
    public C(A a) {
      this.a = Preconditions.checkNotNull(a);
      System.out.println("C() is called.");
    }

    public void run() {
      System.out.println("C#run() is called.");
      a.run();
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(A.class).in(Scopes.SINGLETON);
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    B b = injector.getInstance(B.class);
    b.run();
    C c = injector.getInstance(C.class);
    c.run();
  }
}

出力

A() is called.
B() is called.
B#run() is called.
A#run() is called.
C() is called.
C#run() is called.
A#run() is called.

BとCのインスタンスを作るよう指示しました。それぞれのコストラクタの引数にAが入っているので、injectorはAのインスタンスを生成して渡そうとします。ここでModuleの中でScopes.SINGLETONが指定されているので、injectorはAを1回だけ生成し、それを2回目以降はそのインスタンスを返します。出力からAのコンストラクタが1回しか呼ばれてないのが分かります。

@Singletonアノテーションを使っても同様のことができますが、Scopeをどこで指定したのかわからなくなってしまうためやめたほうが良いと思います。Linked Bindingsと組み合わせることもできます。

Binding Annotations

同じ型の引数に@Namedアノテーションで別の名前をつけることで、別のインスタンスをinjectするよう指定できます。

import com.google.common.base.Preconditions;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;
import com.google.inject.name.Names;

public class Sample05 {
  public interface A {
    void run();
  }

  public static class B implements A {
    public B() {
      System.out.println("B() is called.");
    }

    @Override
    public void run() {
      System.out.println("B#run() is called.");
    }
  }

  public static class C implements A {
    public C() {
      System.out.println("C() is called.");
    }

    @Override
    public void run() {
      System.out.println("C#run() is called.");
    }
  }

  public static class D {
    private final A a0;
    private final A a1;

    @Inject
    public D(@Named("BBB") A a0, @Named("CCC") A a1) {
      this.a0 = Preconditions.checkNotNull(a0);
      this.a1 = Preconditions.checkNotNull(a1);
      System.out.println("D() is called.");
    }

    public void run() {
      System.out.println("D#run() is called.");
      a0.run();
      a1.run();
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(A.class).annotatedWith(Names.named("BBB")).to(B.class);
      bind(A.class).annotatedWith(Names.named("CCC")).to(C.class);
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    D d = injector.getInstance(D.class);
    d.run();
  }
}

出力

B() is called.
C() is called.
D() is called.
D#run() is called.
B#run() is called.
C#run() is called.

Dのインスタンスを作るよう指示しました。injectorはDの引数に"BBB"と名付けられたAと"CCC"と名付けられたAが入っているのを見つけます。ModuleにそれぞれBとCを作るような指示が入っているため、BとCを生成してDのコンストラクタに渡し、Dのインスタンスを生成して返してくれます。

ただし、@Namedを使うとインスタンスの実際の型が分かりにくくなってしまうため、あまり使わないほうが良いかもしれません。

Instance Bindings

injectするインスタンスを手動で生成して直接結びつけます。必然的にSingletonになります。

import com.google.common.base.Preconditions;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;

public class Sample06 {
  public interface A {
    void run();
  }

  public static class B implements A {
    public B() {
      System.out.println("B() is called.");
    }

    @Override
    public void run() {
      System.out.println("B#run() is called.");
    }
  }

  public static class C {
    private final A a;

    @Inject
    public C(A a) {
      this.a = Preconditions.checkNotNull(a);
      System.out.println("C() is called.");
    }

    public void run() {
      System.out.println("C#run() is called.");
      a.run();
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(A.class).toInstance(new B());
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    C c = injector.getInstance(C.class);
    c.run();
  }
}

Cのインスタンスを生成するよう指示しました。injectorはCのコンストラクタ引数にAが入っているのを見つけます。Moduleに型Aの引数にはconfigure()内で作られたインスタンスを渡すように支持があるため、そのインスタンスを渡してCのインスタンスを生成します。

@Named等と併用できます。

@Provides Methods

injectするクラスやインスタンスを指定する代わりに、インスタンスを作るメソッドをModule内に定義すると、そのメソッドを使ってインスタンスを作ってinjectしてくれます。戻り値の型をinjectしたい型にして@Providesをつけるだけです。メソッド引数を加えると、そこにもinjectしてくれます。@Namedや@Singletonと併用できます。

import com.google.common.base.Preconditions;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provides;

public class Sample07 {
  public static class A {
    public A() {
      System.out.println("A() is called.");
    }

    public void run() {
      System.out.println("A#run() is called.");
    }
  }

  public interface B {
    void run();
  }

  public static class C implements B {
    private final A a;

    @Inject
    public C(A a) {
      this.a = Preconditions.checkNotNull(a);
      System.out.println("C() is called.");
    }

    @Override
    public void run() {
      System.out.println("C#run() is called.");
      a.run();
    }
  }

  public static class D {
    private final B b;

    @Inject
    public D(B b) {
      this.b = Preconditions.checkNotNull(b);
      System.out.println("D() is called.");
    }

    public void run() {
      System.out.println("D#run() is called.");
      b.run();
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
    }

    @Provides
    private B provideA(A a) {
      return new C(a);
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    D d = injector.getInstance(D.class);
    d.run();
  }
}

出力

A() is called.
C() is called.
D() is called.
D#run() is called.
C#run() is called.
A#run() is called.

Dのインスタンスを生成するよう指示しました。Dのコンストラクタ引数はBですので、Bのインスタンスを生成して渡そうとします。Moduleに戻り値の型がBで@Providesアノテーションが付いているメソッドがあるので、これを使ってインスタンスを生成しようとします。providerメソッド引数にAがあるので、先にAのインスタンスを生成します。Aのインスタンスを生成しproviderメソッドに渡し、Bのインスタンスを生成します。ここで実際にはCのインスタンスが生成されています。そのインスタンスをDのコンストラクタに渡し、Dのインスタンスを生成して返します。

TypeLiterals

bind()ではジェネリクスを伴ったクラスを直接bindすることができません。これはジェネリクスの型情報がコンパイル時に消えてしまうためだと思います。ジェネリクスを伴ったクラスをbind()する場合は、TypeLiteralで包んで指定します。

import com.google.common.base.Preconditions;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.TypeLiteral;

public class Sample08 {
  public interface A<T> {
    void run(T t);
  }

  public static class B<T> implements A<T> {
    public B() {
      System.out.println("B() is called.");
    }

    public void run(T t) {
      System.out.println("B#run() is called with \"" + t + "\".");
    }
  }

  public static class C {
    private final B<Integer> b;

    @Inject
    public C(B<Integer> b) {
      this.b = Preconditions.checkNotNull(b);
      System.out.println("C() is called.");
    }

    public void run() {
      System.out.println("C#run() is called.");
      b.run(42);
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(new TypeLiteral<A<Integer>>() {
      }).to(new TypeLiteral<B<Integer>>() {
      });
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    C c = injector.getInstance(C.class);
    c.run();
  }
}

出力

B() is called.
C() is called.
C#run() is called.
B#run() is called with "42".

Cのインスタンスを生成するように指示しました。Cのコンストラクタ引数にはA<Integer>が含まれているので、A<Integer>のインスタンスを生成しようとします。Module内で"型A<Integer>の引数にはBのインスタンスを結び付けろ"と指示があるので、Bのインスタンスが生成され、Cのコンストラクタ引数に渡されます。最後にCのインスタンスが生成されます。

Javaはあまり詳しくないため、なぜTypeLiteralでくるむとジェネリクス情報が残るのかわかっていませんorz

AssistedInject

プログラム実行中にクラスのインスタンスの生成をするとき、コンストラクタ引数の一部を手動で渡したい場合があると思います。このような場合はFactoryクラスを経由するのが定石だと思います。FactoryModuleBuilderを使用するとFactoryクラスを自動的に実装してくれます。このとき、引数の一部は手動で、残りはGuiceが自動的にインスタンスを生成して渡す形になります。

FactoryModuleBuilderを使うときは、Factoryインタフェースと@Assistedアノテーションを使います。初めにFactoryインタフェースを定義し、内部にFactoryメソッドを定義しておきます。このとき引数にはコンストラクタプログラム実行時に手動で渡したいものを書いておきます。また戻り値は生成したいインスタンスの型か、その親クラス/インタフェースを書いておきます。

次にFactoryクラスに生成させたいクラスのコンストラクタ引数のうち、手動で渡したいものに@Assistedアノテーションを付けます。Factoryクラスのメソッド引数と@Assistedのついた引数が一致すると、FactoryModuleBuilderが内部でFactoryクラスを自動的に実装してくれます。

Factoryクラスのメソッド戻り値が、欲しいクラスの親クラス/インタフェースの場合は、FactoryModuleBuilder#implement()を使ってどのクラスを生成するか指定します。

import com.google.common.base.Preconditions;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.FactoryModuleBuilder;

public class Sample09 {

  public static class A {
    public A() {
      System.out.println("A() is called.");
    }

    public void run() {
      System.out.println("A#run() is called.");
    }
  }

  public interface B {
    void run();
  }

  public static class C implements B {
    private final A a;

    @Inject
    public C(A a, @Assisted int number) {
      this.a = Preconditions.checkNotNull(a);
      System.out.println("C() is called with " + number + ".");
    }

    @Override
    public void run() {
      System.out.println("C#run() is called.");
      a.run();
    }
  }

  public interface BFactory {
    B create(int number);
  }

  public static class D {
    private final BFactory bFactory;

    @Inject
    public D(BFactory bFactory) {
      this.bFactory = Preconditions.checkNotNull(bFactory);
      System.out.println("D() is called.");
    }

    public void run() {
      System.out.println("D#run() is called.");
      B b = bFactory.create(42);
      b.run();
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
      install(new FactoryModuleBuilder().implement(B.class, C.class).build(BFactory.class));
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    D d = injector.getInstance(D.class);
    d.run();
  }
}

出力

D() is called.
D#run() is called.
A() is called.
C() is called with 42.
C#run() is called.
A#run() is called.

Dのインスタンスを生成するように指示しました。Dのコンストラクタ引数にはBFactoryがありますので、BFactoryのインスタンスを生成しようとします。Moduleの中で「BFactoryの実装をしろ。型Bを戻り値に持つメソッドがあったらCのインスタンスを生成して返せ。」と指定されたModuleがinstallされているので、この通りにBFactoryのインスタンスを自動実装してDのコンストラクタに渡します。このとき、Bのコンストラクタが呼ばれていない点がポイントです。Bのコンストラクタ初期化が終わったあと、BFactory#create()を呼んだときに初めて呼ばれます。

続いてBFactory#create()が呼ばれBのインスタンスが生成されようとしています。BFactoryはBの代わりにCのインスタンスを作るように指定されていましたので、Cのコンストラクタが呼ばれます。CのコンストラクタにはAが指定されていますので、Aのインスタンスが生成されて渡されます。またCの引数numberには、BFactory#create()に渡されたものが渡されます。Cのインスタンスが生成され、BFactory#create()から返ってきます。

implement()にはTypeLiteralも渡せますので、ジェネリクスを含むクラスも指定することができます。またimplement()にはAnnotationが渡せるようになっており、一つのFactoryクラスの中で、@Namedで違う名前を付けた、同じ型の戻り値を持つFactoryメソッドを作ることもできます。ただし、これは複雑すぎるため避けたほうが良いと思います。

Multibindings

Multibindingを使うと、複数のインスタンスをSet<T>に入れた状態でinjectすることができます。やりかたはMultibinderのインスタンスにaddBinding()でインスタンス生成ルールを追加していくだけです。あまり使用機会はないのではないかもしれません。

import java.util.Set;

import com.google.common.base.Preconditions;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.multibindings.Multibinder;

public class Sample10 {
  public interface A {
    void run();
  }

  public static class B implements A {
    public B() {
      System.out.println("B() is called.");
    }

    public void run() {
      System.out.println("B#run() is called.");
    }
  }

  public static class C implements A {
    public C() {
      System.out.println("C() is called.");
    }

    public void run() {
      System.out.println("C#run() is called.");
    }
  }

  public static class D {
    private final Set<A> as;

    @Inject
    public D(Set<A> as) {
      this.as = Preconditions.checkNotNull(as);
      System.out.println("D() is called.");
    }

    private void run() {
      System.out.println("D#run() is called.");
      for (A a : as) {
        a.run();
      }
    }
  }

  public static class SampleModule extends AbstractModule {
    @Override
    protected void configure() {
      Multibinder<A> multibinder = Multibinder.newSetBinder(binder(), A.class);
      multibinder.addBinding().to(B.class);
      multibinder.addBinding().to(C.class);
    }
  }

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new SampleModule());
    D d = injector.getInstance(D.class);
    d.run();
  }
}

出力

B() is called.
C() is called.
D() is called.
D#run() is called.
B#run() is called.
C#run() is called.

Dのインスタンスを生成するように指示しました。Dのコンストラクタ引数にはSetが含まれていますので、injectorはこの生成ルールを探します。Module内でMultibinderでSetBinderが作らていますので、これに従ってBとCのインスタンスを生成してSetに入れてDに渡し、Dのインスタンスを生成します。

次回予告?

  • Web and Servlets Integration
  • Test with Mockito
  • GuiceBerry

あたりをやるかもしれません。

リンク

  • google-guice - Guice (pronounced 'juice') is a lightweight dependency injection framework for Java 5 and above, brought to you by Google. - Google Project Hosting http://code.google.com/p/google-guice/
  • 心地良すぎるモックライブラリ Mockito 〜その1〜 - etc9 http://d.hatena.ne.jp/Naotsugu/20101108/1289218176

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/nodchip/20130126/1359161946
リンク元