ProviderとFactoryProvider
複数のオブジェクトを取得する方法
Injectの方法はコンストラクタによるもの、フィールドによるもの、メソッドによるものとある。
class Test { @Inject Sample1 one; @Inject Test(Sample2 two) { } @Inject void setThree(Sample3 three) { } }
いずれの場合にも、それぞれのオブジェクトはただ一つしか取得できない。逆に言えば、「なされるがまま」である。Testというオブジェクトが生成されたとき、そこで必要なオブジェクトは既に勝手に注入された状態である。Testオブジェクトの中にて、任意のタイミングで任意の個数の他いオブジェクトを生成する方法が必要だが、もちろんnewを使ってはならない(厳密に言えば、newを使ってもよいのはそのクラスがGuiceとは無関係の場合)。 これを行うにはProviderを注入しておく。
public interface UserLookup { public UserInfo lookup(String userId, String password); } .... public class UserLookupImpl implements UserLookup { @Inject Provider<UserInfo>userInfoProvider; public UserInfo lookup(String user, String password) { UserInfo userInfo = userInfoProvider.get(); userInfo.setUser(user); return userInfo; } } .... public class UserInfoImpl implements UserInfo { .... public void setUser(String user) { .... } }
などとしておき、
Injector injector = Guice.createInjector(new AbstractModule() { @Override protected void configure() { bind(UserLookup.class).to(UserLookupImpl.class); bind(UserInfo.class).to(UserInfoImpl.class); } }); UserLookup userLookup = injector.getInstance(UserLookup.class);
とする。UserLookupImpl内ではuserInfoProviderからいくつでもUserInfoオブジェクトを取得できるし、当然のことながらUserInfoオブジェクトの生成を必要になるまで遅らせることができる。
※このとき、「bind(UserInfo.class).to(UserInfoImpl.class);」というバインディング指定だけでGuiceが勝手にProviderを作成してくれることに注意。
Contextualインジェクション1
上記でUserLookupImplは、存在するユーザについてUserInfoを作成し、そのユーザIDをsetUserで設定しているが、setUserでユーザIDを指定するのではなく、UserInfoのコンストラクタ引数として指定できないものだろうか? つまり、現状はこうなっているが、
public class UserInfoImpl implements UserInfo { private String user; public void setUser(String user) { this.user = user; } }
理想としては以下である。ユーザフィールドはfinalとし、このオブジェクト生成後は勝手にuserが変更されないようにしたい。
public class UserInfoImpl implements UserInfo { private final String user; public UserInfoImpl(String user) { this.user = user; } }
しかし、単純に以下のようにすることはできない。
public class UserInfoImpl implements UserInfo { private final String user; @Inject public UserInfoImpl(String user) { this.user = user; } }
上のように書いてしまうと、Guiceは何をコンストラクタに与えるべきかわからないし、そもそもProviderにはget()という引数無しのメソッドしかないので、生成時に引数指定の方法がない。 また、bindでこれらの文字列を指定しても全く意味がない。与えるべき文字列は「UserLookupImpl.lookup(String user, String password)」というメソッドに渡された文字列であり、モジュール内でそれらを指定することはできない。
このような操作、つまり「そのコンテキストで発生したオブジェクト」を注入することを、Contextualインジェクションと呼ぶらしい。
Contextualインジェクション2
以下を行うには、guice-assisted-inject*.jarが必要である。また以下の例はGuice 2.0の場合。
まず、UserInfImplを変更する。userパラメータはコンストラクタで受け取るが、それに@Assistedというアノテーションをつける。userフィールドはfinalとする。 ※もし@Assistedアノテーションのないパラメータが存在する場合は、それはGuiceが勝手に注入する。
public class UserInfoImpl implements UserInfo { private final String user; @Inject public UserInfoImpl(@Assisted String user) { this.user = user; } }
UserInfoFactoryというインターフェースを定義する。これはGuiceの提供するProviderの代わりに用いられるものである。Provider.get()では引数を指定することができないが、(自分で作成したインターフェースであるので当然だが)、どのような引数も指定することができる。
public interface UserInfoFactory { UserInfo getUserInfo(String user); }
UserLookupImplはProviderではなく、上のUserInfoFactoryを使ってUserInfoを生成するように変更する。引数としてユーザIDを指定することができる。
public class UserLookupImpl implements UserLookup { @Inject UserInfoFactory userInfoFactory; public UserInfo lookup(String user, String password) { UserInfo userInfo = userInfoFactory.getUserInfo(user); return userInfo; } }
モジュールでは以下の指定を行う。
bind(UserLookup.class).to(UserLookupImpl.class); // bind(UserInfo.class).to(UserInfoImpl.class); これは不要 bind(UserInfoFactory.class).toProvider( FactoryProvider.newFactory(UserInfoFactory.class, UserInfoImpl.class));
FactoryProvider(Guice 2.0 API)の抄訳
まとめ
「オブジェクトが勝手に生成され、勝手に注入される」というのがDIの基本原則といった説明があちこちで見られる。「ハリウッド原則」と言ってアプリ側からはフレームワークの機能(オブジェクトの生成)を呼び出すことがないというが、これは誤解を生じさせる可能性がある。 当然のことだが、勝手に生成され勝手に注入されるオブジェクトだけではプログラミングなどできようはずもない。
このような「必要な時に必要なオブジェクトを作成したい」という用途には、Providerや自作のFactoryを用いる。 しかしながら、Providerでは、オブジェクト生成時にこちらから引数を指定できない。対象とするクラスにコンストラクタ引数が指定されていても、それらはすべてGuiceが勝手に生成して注入するためのものである。 その一方で、Factoryを用いれば、生成時にこちらから引数を指定することもできるし、Guiceに勝手に引数オブジェクトを生成させることもできる。