前回の記事で紹介したのは、Google GuiceのDI機能のうちの一つ、Linked Bindingsだけでした。この記事はその続きです。
サンプルはScalaで書きましたが、例によってScalaっぽいコードが出てくるわけではないですね……Guiceをどう使えばScalaとよりよく連携できるようになるかは、まだよく分かっていません1。
さて、Guiceはもうちょっと色々な方法でDIができるので、それについても調べて紹介します。
以下では例示のために、前回も使用したService
とその実装クラス2つを使います。
trait Service {
def doSomething(): Unit
}
class FakeService extends Service {
def doSomething() { println("FakeService") }
}
class RealService extends Service {
def doSomething() { println("RealService") }
}
用語
あるインタフェースに、特定の実装を結びつけることをバインディング(binding)と呼ぶようです。
バインディングを柔軟に制御することがDIフレームワークの目標です。
さまざまなバインディング方式
Binding Annotations
Linked Bindingでは、一つの型に一つの具体的な実装を配線させることができました。
Binding Annotationは、バインディングの配線条件をより細かくすることができる機能です。条件には@Named
アノテーションを利用します。例えば「型Aは実装Bを使うか、実装Cを使うかをアノテーションによって変える」といったことができます。
class ServiceUser @Inject() (@Named("realService") service: Service) {
def use() { service.doSomething() }
}
class AnotherServiceUser @Inject() (@Named("fakeService") service: Service) {
def use() { service.doSomething() }
}
class ServiceModule extends AbstractModule {
override protected def configure() {
bind(classOf[Service]).annotatedWith(Names.named("realService")).to(classOf[RealService])
bind(classOf[Service]).annotatedWith(Names.named("fakeService")).to(classOf[FakeService])
}
}
また、カスタムのアノテーションを作ることもできます。
実行例はこんな感じになります。
def run() {
val injector = Guice.createInjector(new ServiceModule)
val user = injector.getInstance(classOf[ServiceUser])
val another = injector.getInstance(classOf[AnotherServiceUser])
user.use() // => "RealService"
another.use() // => "FakeService"
}
Instance Bindings
ある型をある型にバインディングするのではなく、ある型に具体的なインスタンスをバンディングする機能です。@Named
などと同時に使用することもできます。
class ServiceUser @Inject()(service: Service) {
def use() {
println("received: " + service)
}
}
class ServiceModule extends AbstractModule {
override protected def configure() {
val service = new RealService
println("bound: " + service)
bind(classOf[Service]).toInstance(service)
}
}
例によってInjector
を準備してインスタンスを生成・実行してみると、確かに.toInstance
で指定したインスタンスがServiceUser
に渡っていることが分かります。
def run() {
val injector = Guice.createInjector(new ServiceModule)
val user = injector.getInstance(classOf[ServiceUser])
user.use()
}
bound: RealService@16f332ef
received: RealService@16f332ef
このバインディング方式は、複雑なオブジェクトの生成には向きません。
その場合には@Provides
が推奨2されます。
ProvidesMethods
オブジェクトの生成をModule
内で実装できるようにします。@Provides
アノテーションを付与することで、そのメソッドがインスタンス生成に利用されるようになります。
class ServiceUser @Inject()(service: Service) {
def use() {
service.doSomething()
}
}
class ServiceModule extends AbstractModule {
override protected def configure() {}
@Provides
def provideRealService(): Service = new RealService
}
なお、プロバイダメソッドから例外を投げるのは推奨されないようです。
ProviderBindings
ProvidersMethodsがファクトリメソッドを作ってインスタンス生成を分離する機能だったのに対して、インスタンス生成を別クラスに分離するのがProviderBindingsです。より生成が複雑化した場合などに用いられるようです。
生成を行うクラスは、Provider[T]
を実装するのがルールです。このクラスはT
を返すメソッドget()
を実装さえすれば機能します。
class RealServiceProvider extends Provider[Service] {
override def get(): Service = new RealService
}
生成の委譲は.toProvider
を記述することで行えます。
class ServiceModule extends AbstractModule {
override protected def configure() {
bind(classOf[Service]).toProvider(classOf[RealServiceProvider])
}
}
Constructor Bindings
型を型ではなく、型を指定したコンストラクタにバインドすることもできます。
例えば、依存性を注入する先のクラスのコンストラクタが複数ある場合に利用できます3。
また、AOPなどでメタプログラミングをする場合にも必要なケースがあるとのことです。
class ServiceUser @Inject()(service: Service) {
def use() {
service.doSomething()
}
}
class ServiceModule extends AbstractModule {
override protected def configure() {
bind(classOf[Service]).toConstructor(classOf[RealService].getConstructor())
}
}
Just-In-Time Bindings
ある型に対する実装が必要になったときでも、モジュールにバインドの記述がない場合、明示的バインディング(explicit bindings)ではなく暗黙のバインディング(Just-In-Time bindings / implicit bindings)が試みられます。
暗黙のバインディングには、明示的なコンストラクタと呼ばれるものと、@ImplementedBy
を使うもの、@ProvidedBy
を使うものがあります。
明示的コンストラクタ
class RealService (@Named("serviceName") name: String) extends Service {
override def doSomething(): Unit = println("service name is " + name)
}
class ServiceUser @Inject()(service: Service) {
def use() {
service.doSomething()
}
}
class ServiceModule extends AbstractModule {
override protected def configure(): Unit = {
bind(classOf[Service]).to(classOf[RealService])
}
@Provides
@Named("serviceName")
def provideServiceName: String = "ServiceName!!"
}
この例では、RealService
についてのバインディングがServiceModule
に明示的に記述されているわけではないにもかかわらず、RealService
インスタンスへname
が注入される形で全体のインスタンス化が行われます。
対象のクラスは、publicなコンストラクタを1つのみ持ち、かつコンストラクタ引数はたかだか1つ(0 or 1)でなければならないです。
@ImplementedBy
抽象に対するデフォルト実装をアノテーションで指定することができます。
@ImplementedBy(classOf[AnotherFakeService])
trait AnotherService {
def doSomething(): Unit
}
class AnotherFakeService extends AnotherService {
override def doSomething(): Unit = println("AnotherFakeService")
}
class ServiceModule extends AbstractModule {
override protected def configure() {}
}
上記コードを実行する実装を示します。
def run() {
val injector = Guice.createInjector(new AbstractModule {
override def configure(): Unit = {}
})
val service = injector.getInstance(classOf[AnotherService])
service.doSomething() // => "AnotherFakeService"
}
つまり、Module
にbind(〜).to(...)
で実装を指定するのと同様の効果をインタフェースへの指定で実現できます。ただし、bind
で指定された明示的なバインディングがある場合には、そちらが優先されます。
@ProvidedBy
@ImplementedBy
が具体的なデフォルト実装を指定するものだったのに、対して、@ProvidedBy
はデフォルトのプロバイダを指定します。Module
にbind(〜).toProvider(...)
指定をするのに相当します。明示的なバインディングがある場合はそちらが優先されるのも同様です。
@ProvidedBy(classOf[AnotherRealServiceProvider])
trait AnotherService {
def doSomething(): Unit
}
class AnotherRealService extends AnotherService {
override def doSomething(): Unit = println("AnotherRealService")
}
class AnotherRealServiceProvider extends Provider[AnotherService] {
override def get(): AnotherService = new AnotherRealService
}
Untargeted Bindings
バインドする先を指定しないbind
を記述することができます。Guiceにバインドがあることを伝えられるので、依存関係をeagerに解決することができるようになります。
もちろん、通常は別の方法(例えば@ImplementedBy
)で依存関係が記述されている事が前提です。
bind(classOf[Service]) // .toがないのでuntargeted binding
bind(classOf[Service]).in(classOf[Singleton]) // スコープ指定(後述)をすることができる
Built-in Bindings
java.util.logging.Logger
など、いくつかGuiceが組み込みで把握しているバインディングがあります。詳細についてはドキュメントを参照してください。
スコープ
オブジェクトの生存区間(lifetime)を表すために、Guiceではスコープが導入されています。スコープの指定に従って、Guiceは新しくインスタンスを生成するか、使い回しをするか決めます。
スコープを指定するアノテーションには@Singleton
、@SessionScoped
、@RequestScoped
があり、実装クラスにアノテーションすることでスコープを指定できます。それぞれがアプリケーション、セッション、リクエストを範囲とした生存区間を持ちます。
アノテーションの適用例です。
@Singleton
class RealService extends Service {
def doSomething() {
println("This is RealService")
}
}
bind
の際に、.in
による指定も可能です。
bind(classOf[Service]).to(classOf[RealService]).in(classOf[Singleton])
ちなみに、スコープはカスタムする事も可能そうです。
Stageの切り替え
インスタンスの生成をeagerに行うか、lazyに行うかを、ステージと呼ばれる設定項目を切り替えることで変更できます。
ステージにはPRODUCTION
とDEVELOPMENT
があります。開発時にはコンパイルしてから動作するまでの時間を短くするために、lazyなロードが向いています。一方でプロダクション動作の際にはeagerなロードが行われるようになっています。
ステージは、Guice.createInjector
時に引数としてStage
を渡すことで指定することができます。
Guice.createInjector(Stage.PRODUCTION, module)
EagerなSingleton
バインド時に.asEagerSingleton()
を指定することで、ステージの設定にかかわらず、eagerにオブジェクトをロードさせることができます。
bind(classOf[Service]).to(classOf[RealService]).asEagerSingleton()
JSR-330との関係
Guiceが提供するアノテーションと、JSR-3304で定義されている@Inject
などのアノテーションには微妙に違いがあるため、importを間違えるとうまく動かなくなることがあります。
特に注意が必要なのは@Inject
とProvider周りです。名前が同じで振る舞いが微妙に異なるからです。詳細はGuice公式のドキュメントを参照してください。
参考資料
-
もっとも、少なくともJavaとScalaの相性が良いのと同程度には、GuiceとScalaの相性も良いです。 ↩
-
公式ドキュメントいわく、"Avoid using
.toInstance
with objects that are complicated to create, since it can slow down application startup. You can use an@Provides
method instead." ↩ -
Guiceは依存性の解決時に、目標のクラスに複数コンストラクタがある場合、どのコンストラクタを使えばいのか判断できなくなります。単純な場合だと、
@Provides
などで生成を分離することでも対応できます。 ↩ -
JSR-330はDIについてのJava標準です。 ↩