(その10) 同じ型で異なる実装クラスをinjectしたい場合

いくつか手段があります。

以下、それぞれ見ています。

アノテーションそのもので区別

どの実装をinjectするかを、アノテーションで指定する方法です。まずインタフェースと実装クラス。

public interface Service {
	String getResponse(String msg);

	/** 実装その1 */
	class TypeAServiceImpl implements Service {
		public String getResponse(String msg) {
			return "TypeA : " + msg;
		}
	}
	
	/** 実装その2 */
	class TypeBServiceImpl implements Service {
		public String getResponse(String msg) {
			return "TypeB : " + msg;
		}
	}
}

次に、injectされるクラス。フィールド、コンストラクタ、メソッドでそれぞれ指定

import sample.guice.annotatingbindings.Main.TypeA;
import sample.guice.annotatingbindings.Main.TypeB;
import com.google.inject.Inject;

public class Client {
	@Inject @TypeA
	private Service serviceA;
	@Inject 
	private @TypeB Service serviceB;
	
	@Inject	// ここに@TypeAとは書けない
	public Client(@TypeA Service serviceA) {
		System.out.println("Constractor:" + serviceA);
	}
	
	public void execute() {
		System.out.println(serviceA.getResponse("Hello"));
		System.out.println(serviceB.getResponse("Hello"));
	}
	
	@Inject // ここに@TypeBとは書けない
	public void injectedMethod(@TypeB Service service) {
		System.out.println("Method:" + service);
	}
}

そして起動クラス。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import sample.guice.annotatingbindings.Service.TypeAServiceImpl;
import sample.guice.annotatingbindings.Service.TypeBServiceImpl;
import com.google.inject.AbstractModule;
import com.google.inject.BindingAnnotation;
import com.google.inject.Guice;
import com.google.inject.Injector;

public class Main {
	public static void main(String[] args) {
		Injector injector = Guice.createInjector(new AbstractModule() {
			@Override
			protected void configure() {
				// 実装その1をアノテーション指定でバインド
				bind(Service.class)
					.annotatedWith(TypeA.class)
					.to(TypeAServiceImpl.class);
				// 実装その2を(以下略)
				bind(Service.class)
					.annotatedWith(TypeB.class)
					.to(TypeBServiceImpl.class);
			}
		});
		// 実行
		injector.getInstance(Client.class).execute();
	}
	
	/** 判別用のアノテーション定義その1 */
	@Retention(RetentionPolicy.RUNTIME)
	@Target({ElementType.FIELD, ElementType.PARAMETER})
	@BindingAnnotation
	public @interface TypeA {}
	
	/** 判別用のアノテーション定義その2 */
	@Retention(RetentionPolicy.RUNTIME)
	@Target({ElementType.FIELD, ElementType.PARAMETER})
	@BindingAnnotation
	public @interface TypeB {}
}

実行。

Constractor:sample.guice.annotatingbindings.Service$TypeAServiceImpl@10385c1
Method:sample.guice.annotatingbindings.Service$TypeBServiceImpl@30c221
TypeA : Hello
TypeB : Hello

予想通りの結果です。なお、この場合@TypeAも@TypeBも指定しないでServiceをinjectしようとすると、実行時にエラーになります。*1。その場合、bind(Service.class).to(TypeAServiceImpl.class);として、アノテーション指定無しのバインドも行っておくと、ちゃんとinjectされます。その7で書いた @ImplementByを指定してもOKでした。動きが想像し易いライブラリは楽しいですね。
次に、もう1つの実装クラス指定方法です。

アノテーションで指定する文字列で区別

@Namedというアノテーションを使って、文字列でinjectする実装クラスを区別します。*2
Serviceは同じなので、injectされる側。

import com.google.inject.Inject;
import com.google.inject.name.Named;

public class Client {
	@Inject @Named("タイプA")
	private Service serviceA;
	@Inject 
	private @Named("タイプB") Service serviceB;
	//以下同じ感じ
}

そして起動クラス

	//...
	bind(Service.class)
		.annotatedWith(Names.named("タイプA"))
		.to(TypeAServiceImpl.class);
	bind(Service.class)
		.annotatedWith(Names.named("タイプB"))
		.to(TypeBServiceImpl.class);
	//...

結果は、1つ目のの場合と同じでした。

2つの方法の比較

ちょっと考えてみました。

アノテーション指定のメリット
  • タイプセーフ。Guiceの思想から言ってもメリットの大きさから言っても、これが一番でしょう。
  • IDEのサポート(補完)を受けられる
  • 型で検索できるので、参照している場所を簡単に特定できる
アノテーション指定のデメリット
  • アノテーションの定義が僅かに面倒(でもコピペして名前変えるだけ)
  • アノテーションなので、クラス名に使える名前しか使えない。文字列指定の場合は何でも良いので、後で分かり易いような名前をつけられるかも?

という事で、せっかくGuiceを使うのに文字列指定は勿体無いので、前者の方法が良さそうです。アノテーションを実装のまとまりのセットと考え、同じアノテーションを色々なインタフェースの指定に使えば、簡単だしアノテーションも無駄に増えないと思います。

とは言え、そもそも実装を複数使うケースが実は私にはあまり思いつかないです。これはどういった場合に有効なんでしょう。
1つ思いついたのは、Swingで画面を組み立てる時に、同じ画面部品だけど中身が異なるようなものをバインドする時は良いかもしれません。私の場合(Webアプリ)を考えると、すぐには適切な例が浮かびませんでした。動的な切り替えはStrategyだし…。良い使い所が思い当たる方は是非どこかに書いて頂きたいです。

追記

「文字列での指定」と書きましたが、User's Guideで紹介されているのがStringを属性に持つアノテーションというだけで、他の型を属性にして、それをバインディングのKeyにする事も可能でした。なので、「アノテーションの属性による指定」が正しかったです。

*1:但しASMの例外をラップしているだけで、何が原因か例外を見ただけだと分かり辛いです

*2:追記をご覧下さい