subcut/Getting startedの翻訳
2011/7/1現在のhttps://github.com/dickwall/subcut/blob/master/GettingStarted.mdの翻訳。
このドキュメントではsubcutのはじめ方を説明する。ライブラリのダウンロード、最初のバインディングモジュールのセットアップ、注入可能なクラスの作り方に、implicitメソッドを使って注入可能クラスに設定を伝える(訳注:?)方法。ここではsubcutの推奨される使い方に注力する。簡単に言えば、
- イミュータブルなバインディングモジュール
- trait及びtrait/nameキーによるバインディング
- implicitバインディングモジュールによる注入可能クラス
- injectIfBoundセマンティクスの使用(特定のtraitに対するコンフィギュレーションがあれば注入するが、そうでなければinjectIfBoundの右側の定義を使用する)。
- バインディングモジュールのミュータブルなコピーによるテスト
他にもsubcutを利用する方法はたくさんある。明示的な注入、コンストラクタ注入、mixinコンフィギュレーション、あるいはミュータブルなバインディングモジュールである(これを使用するのは非常な注意が必要)。
簡単な説明
これはハイレベルな外観であり、subcutを使うチートシートとも言える。 詳細についてはもっと読み進んでほしい。
これは、機能させるための単なる一つレシピであり、私のおすすめでもある。 subcutを使う方法は他にもある。
- subcutを依存に加える(訳注:mavenの依存関係などに入れるということ)、あるいはjarファイルをダウンロードする。subcutはScalaランタイムライブラリ以外の依存は持たない。
- 以下のようにバインディングモジュールを作成する。
object SomeConfigurationModule extends NewBindingModule({ module => module.bind[X] toInstance Y module.bind[Z] toProvider { codeToGetInstanceOfZ() } })
注入したいすべてのクラスについて、あるいは新規に注入可能にしたいインスタンスについて、次のクラス宣言を追加する:(implicit val bindingModule: BindingModule)とInjectableというtraitだ。つまり、
class SomeServiceOrClass(param1: String, param2: Int)(implicit val bindingModule: BindingModule) extends SomeTrait with Injectable {...}
- バインディングを注入したいクラスの中で、次のコード、あるいは同様のものを使う。この関数の右側はマッチするバインディングが存在しない場合のデフォルトとして使用される。
val service1 = injectIfBound[Service1] { new Service1Impl }
- テスト時には、通常のイミュータブルなバインディングモジュールから変更可能なバインディングを作成して再バインディングする。
test("Some test") { SomeConfigurationModule.modifyBindings { testModule => testModule.bind [SomeTrait] toInstance new FakeSomeTrait val testInstance = new SomeServiceOrClass(param1, param2)(testModule) // test stuff... } }
あなたのプロジェクトにsubcutを含ませる
もし、MavenやSBT以外のプロジェクトコンフィギュレーションを用いているのであれば、単にmavenリポジトリのscala-toolsにあるsubcutの.jarファイルをダウンロードし、それをクラスパスに加えてほしい。
Mavenの場合
<dependency> <groupId>org.scala-tools</groupId> <artifactId>subcut_2.9.0</artifactId> <version>0.8</version> </dependency>
_2.9.0は、あなたの利用するScalaのバージョンにする(ただし、2.9.0-1の場合は_2.9.0にすること。このバージョンの別のビルドは存在しない)。 0.8は最新のsubcut安定バージョンにしてほしい。 scala-toolsのスナップショットリポジトリにスナップショットビルドも用意されている。 もし、scala-toolsのリポジトリが見つからない場合は、http://scala-tools.orgを見てmavenリポジトリリストに加えること。
sbtの場合
バージョンとリポジトリ構成についてはmaven用のものを参照のこと。 subcutを使うには、次の依存をプロジェクトに追加する。
val subcut = "org.scala-tools" %% "subcut" % "0.8"
0.8は最新(あるいは所望の)subcutのバージョンにする。
コンフィギュレーションモジュールをセットアップする
ここでは単一のバインディングモジュールをセットアップして使うおすすめの方法を説明し、 便利な共通のバインディングをデモする。 さらなる可能性については、このセクションの最後を参照すること。
新たにイミュータブルなバインディングモジュールを作成するには次のようにする。
object ProjectConfiguration extends NewBindingModule({ module => module.bind [Database] toInstance new MySQLDatabase module.bind [Analyzer] identifiedBy 'webAnalyzer to instanceOfClass [WebAnalyzer] module.bind [Session] identifiedBy 'currentUser toProvider { WebServerSession.getCurrentUser().getSession() } module.bind [Int] identifiedBy 'maxThreadPoolSize toInstance 10 module.bind [WebSearch] toLazyInstance { new GoogleSearchService()(ProjectConfiguration) } })
上のバインディングの意味を次に説明する。
Databaseは、NewBindingModule作成時に作成されたMySQLDatabaseのただ一つのインスタンスにバインドされる。 このバインディングでは、いつも同じインスタンスが返されるので、スレッド化環境ではこの単一のインスタンスがスレッドセーフでなくてはいけない。
webAnalyzerという識別子を付加されたAnalyzerは、WebAnalyzerにバインドされ、それが要求される度にWebAnalyzerの新たなインスタンスがリフレクションを通して生成されて提供される。WebAnalyzerは引数0の(デフォルトの)コンストラクタを持たなければならないことに注意。
currentUserと名付けられたSesionは関数にバインドされ、バインドの使用ごとにその関数が呼び出されてSessionを提供する。 これはインスタンス生成の最も柔軟性のあるやり方である。なぜなら、プロバイダメソッドは、それが返すインスタンスがSessionトレイトのサブタイプである限り、いかなることをもすることができるからである。
maxThreadPoolSizeとして識別されるIntは常に10というInt値を返す。 IntやStringのような共通タイプを識別名無しでバインドすることはおすすめできない。 そのバインディングが非常に広範囲になってしまい、知らないうちに値をピックしてしまうことがあるからだ。
最後のWebSearchは、供給メソッドで生成される単一のインスタンスにlazilyにバインドされる。 これについては二つの注意事項がある。 toLazyInstanceは、それが最初にバインドされるまで生成を引き伸ばすが、その後は同じインスタンスを常に返す。
このケースでは、二つ目の理由のためにlazyバインディングが必須である。 GoogleSearchServiceコンストラクタには、第二カリー化パラメータとしてバインディングコンフィギュレーションが供給されなければならないからだ。 バインディングコンフィギュレーション中にピックアップ可能なimplicitバインディングが存在しないのである。 そのコンフィギュレーションモジュールそれ自体の定義前にそれを使うことの内容、lazinessが要求される。
これは混乱を招くかもしれないが、後に述べるimplicitバインディングアプローチを読むまで心配しないで欲しい。
ここでのバインディングモジュールはシングルトンオブジェクトであることに注意。 これが物事をナイスでシンプルな状態に保つおすすめの方法である。 あなたのプロジェクトのどこかのパッケージでこのような定義を行い、そこでコンフィギュレーションを示し、探しやすくしておくこと。
Injectableなクラスを作成する
これらのバインディングを使うおすすめの方法は以下である。
class DoStuffOnTheWeb(val siteName: String, val date: Date)(implicit val bindingModule: BindingModule) extends Injectable { val webSearch = injectIfBound[WebSearch] { new BingSearchService } val maxPoolSize = injectIfBound[Int]('maxThreadPoolSize) { 15 } val flightLookup = injectIfBound[FlightLookup] { new OrbitzFlightLookup } val session = injectIfBound[Session]('currentUser) { Session.getCurrent() } def doSomethingCool(searchString: String): String = { val webSearch = webSearch.search(searchString) val flight = flightLookup(extractFlightDetails(webSearch)) // ... } }
定義についていくつか注意
DoStuffOnTheWebクラスは通常のコンストラクタパラメータリストを持つので、インスタンス生成時にはそれらを提供しなければならない。つまり、injectableでないクラスと同様にnew DoStuffOnTheWeb(site, date)と呼び出す必要がある。
bindingModuleというimplicitパラメータは、バインディングコンフィギュレーションをクラスに注入する方法を示す。これをimplicitとすることにより、コンパイラは、コンパイル時にこれを埋め込む。これがsubcutの裏側の「マジック」だ。コンパイラが我々の代わりに注入を行ってくれるのである。アプリケーションのトップレベルでimplicitを宣言しておき、injectableクラスについてnewを呼び出すことにより、注入が上から下まで行われる(?)。いかなるインスタンスも他のインスタンスについてnewを呼び出すことができるのは、implicitがスコープ内にあるからであり、コンパイラはそれを埋め込む方法を知っているのである。そして、トップを含めてどの時点でもクラスに明示的な異なるバインディングを供給することができ(単に、バインディングを持つ第二パラメータを提供すればよい)、それは残りのすべての部分に影響を与える。もしこのチェインが断ち切れたなら、コンパイラはバインディングモジュール定義が無いことを伝えてくれる。これを修復するには、injectableトレイトとimplicitを追加すればよい。implicitであるBindingModleはbindingModuleという名称でなくてはいけない。この値はInjectableトレイトが必要とするからである。bindingModuleが定義されていないとコンパイルエラーが発生する。これにより、あなたの注入バインディングの途切れないチェインを、それを必要とする最下層のクラスにまで伝えることができる。
- injectableなクラスには、Injectableトレイトが現れなければならない。ここにinjectIfBoundのようなすべての注入メソッドが定義されている。
- クラスのトップにあるinjectIfBound[Trait]のvalは、コンフィギュレーションバインディングが使用されている場所である。injectIfBoundは、もしそれが提供されていればそれを使用するが、そうでなければ右側の式にあるデフォルトが提供される。したがって、例の中に以下のような行があるが、
val session = injectIfBound[Session]('currentUser) { Session.getCurrent() }
subcutは'currentUserというIDを持つSessionトレイトのバインド定義があるかを調べ、あればそれを使う(この場合、Session.getCurrent()は評価もされず、使いもされない)。これに対し、次の場合
val flightLookup = injectIfBound[FlightLookup] { new OrbitzFlightLookup }
subcutはFlightLookup(追加ID名の指定無し)を探してバインドがなければ、右側のデフォルト式を評価して新たなインスタンスを返す。これは我々のクラスが生成される度に毎回行われる。
OrbitzFlightLookupは、それ自体をinjectedクラスにすることができる。バインディングのためのimplicitパラメータはスコープ無いにあるため、コンパイラは自動的にそれを適用する。つまり、OrbitzFlightLookupは以下のように定義することが可能だ。
class OrbitzFlightLookup(implicit val bindingModule: BindingModule) extends FlightLookup with Injectable { ... }
OrbitzFlightLookupは、バインディングを満たすためにFlightLookupTraitをミックスしなければならないことに注意(訳注:FlightLookupトレイトの間違い?)。また、コンストラクタパラメータを持たなくとも、implicitパラメータは必要であることに注意する。
クラスの残りの部分では、これらの注入された値を通常通りに使うことができる(訳注:かなりてきとう)。 もちろん、コンストラクタコードに限らず、クラス内のいかなる部分でも値を注入することができるし、 クラス内でInjectableなインスタンスを作成すれば、自動的にバインディングコンフィギュレーションが受け渡される、 明示的にそれをオーバライドしない限りは。
injectIfBoundは、subcutの提供する一つのカタチに過ぎない。 inject[Trait]は常にbindingModule定義から取得したトレイトを注入し、もしそれがなければ失敗する。 もう一つのカタチはinjectIfMissing[Trait]である。 これは、コンストラクタパラメータとして供給されていない場合にだけインスタンスを注入するというものである (この使い方については、scaladocを参照してほしい)。 これら二つは、バインディングが提供されていなければ失敗してしまうが、injectIfBoundはバインディングがなければデフォルトにフォールバックする。
subcutを使うおすすめの方法がこれであるのは、このような理由が一つある。ランタイム障害を避けることができるためだ。 もう一つの理由としては、そのコードを読みやすくなるということだ。 なぜなら、あるトレイトの「通常の」実装が何であるかがそこに記述されているからだ。
injectIfBoundを使うえば、もう一つ、製品リリースの際に完全に空のBindingModuleで良くなるという可能性がある、実際にこれは私がやっていることだけれども。
(訳注:ということは、この人はDIをテスト目的のためにしか使用していない。これがDIのまっとうな使い方であるというものだ.。DIにはテスト目的以外の価値は全く無い)。
バインディングモジュールを提供しなければならず、テスティングや他の目的のためにオーバーライドするかもしれないが、BindingModuleを空にしておくことは、デフォルトが必ず使用されるということである。 そして、そうすることは若干のパフォーマンス上のメリットもある。bindingModuleが空なので、バインディングの際のルックアップが最適化されるのだ。もちろんいつでもデフォルトの振る舞いを変更するためにオーバライドすることができる。
あなたのBindingModuleを使う
バインディングモジュールを作成して、バインディングをトレイトに注入する方法を示した。 最後のピースはドットを接続することである。
Injectableなクラスに対するimplicit定義(implicit val bindingModule: BindingModule)を使い、特定のモジュールを使うために二つのうちの一つをする必要がある。
一つは、トップクラスのインスタンス生成の前に、implicitな値を作成することである。次のように、
implicit val bindingModule = ProjectConfiguration val topInstance = new DoStuffOnTheWeb("stuff", new Date())
いずれの場合でもバインディングモジュールはDoStuffOnTheWebクラスと、その中で生成されるインスタンスに自動的に供給される。(これが単一の代入でプロジェクトワイドなコンフィギュレーションを供給する方法である)。
もう一つとして、明示的に与える方法もある。
val topInstance = new DoStuffOnTheWeb("stuff", new Date())(ProjectConfiguration)
明示的な方法は最初のインスタンスに対してのみ必要である。 それ以下のインスタンスに対しては暗黙的に供給されるからだ(パラメータリスト中にimplicitで定義され、クラススコープ内でimplicitとして扱われる)。短い形式の方は、その上のimplicit形式のものと全く同じであるが、少しタイプ数が少なくなる。 またこれは、ツリー中の任意の地点でimplicitなバインディングをオーバライドする方法でもある(単に明示的に供給すればよい)し、また新たなバインディングモジュールの作成中に、新たに生成したオブジェクトに対して明示的なバインディングを指定する方法でもある(定位中のバインディングを使い、そのバインディングをlazyにするかproviderにする。これはそのモジュールが利用可能になる以前に使ってしまうことを避ける)。
このチェインが断ち切られてしまうこともあるだろうが、もしそうなればコンパイラはエラーを出す。 例えば、
Class A is Injectable with implicit binding Class B is not Injectable Class C is Injectable with implicit binding Class D is not Injectable
トップレベルクラスAが、Bを生成するとする。これはうまくいく。 そして、BがCとDを問題なく生成するとする。
しかし、クラスBはクラスCのインスタンスを、明示的に何らかのコンフィギュレーションを提供することなしに生成することはできない。
クラスBはimplicitなバインディングモジュールを持っていない(なぜなら、パラメータ中にimplicitバインディングを持たないから)。だから、クラスCの生成時にimplicitバインディングを使うことができない。一方でクラスBはクラスDのインスタンスを生成することはできる。なぜなら、こちらもバインディングモジュールを必要としないからだ。
言い換えれば、implicitを使う限りにおいてはチェインが断ち切られていてはいけないが、それ以上の必要性はない(ある時点から、小さな葉クラスを扱うようになり、それらは何も注入される必要がなく、注入されるようなクラスを扱わない。この時点からInjectableやimplicitを追加する必要はなくなる)。
上のような状況では、コンパイルができなくなり、 クラスCやクラスBのインスタンス生成時にコンパイルエラーが発生する。 必要なbindingModuleが供給されていないからだ。 このようにして、コンパイラは必要なかぎりにおいてチェインを確認する手伝いをしてくれる。
問題を解決するには、単純にimplicit val bindingModule: BindingModuleをクラスBのコンストラクタパラメータとして追加する。
- You don't even need to make B injectable if you don't need it to be, just so long as the implicit is carried through. This will keep the implicit chain intact through to class C, and class D doesn't need either.
他ライブラリとの統合
subcutはsubcutのことを知らない他のライブラリとも簡単に統合することができる。 二つの異なるメカニズムによるbindingModuleコンフィギュレーションを提供することにより、これが可能である。 例えば、新規ページインスタンスの生成をコントロールできないwicketのようなライブラリも含まれる。
(訳注:wicketでは、アプリケーションプログラマ側はページクラスを指定することしかできず、そのnew呼出はwicket内部で勝手に行われてしまう。これを制御する方法は提供されていない。現在でも?)。
伝統的な問題としてはこうだ。インスタンス生成を制御することができないのに、どうやってライブラリ中で生成されたトップレベルのインスタンスにバインディングコンフィギュレーションを指定することができるのだろうか? 通常なら、ライブラリはある種の統合プラグインを提供し、それによりコンフィギュレーションを提供することができるべきだろう。
subcutには簡単な方法がある。単純にInjectableなクラスの新しいサブクラスを定義し、そのサブクラスコンストラクタ中でバインディングモジュールを与えるのである。つまり、
class SomePage(implicit val bindingModule: BindingModule) extends WicketPage with Injectable { } class ProdSomePage extends SomePage(ProjectConfiguration)
こうしておいて、ProdSomePageの方をwicketライブラリに登録する。これは常に同じコンフィギュレーションにバインドされてしまうのだが、何にしても通常は製品リリースの際にはそうするだろう。
subcutとテスト
ここまでで、あなた自身のカスタムなバインディングを任意のInjectableインスタンスに供給することができるはずだ。 そして、それらのバインディングはその地点から、新規インスタンス階層を下って使用されることになる。 ただ、テストに便利なように、ここに若干の機能拡張をしなければならない。そこでは、バインディングの変更がよく行われるからだ。
典型的なテストのためのsubcutのオーバーライドは次のようになる。
test("Test lookup with mocked out services") { ProjectConfiguration.modifyBindings { module => // moduleは一般バインディングのミュータブルなコピーを持つが、これを好きにリバインドする。 module.bind[Int] identifiedBy 'maxThreadPoolSize to None // アンバインドしてデフォルトを使うようにする module.bind[WebSearch] toInstance new FakeWebSearchService // 他で定義されているフェイクサービスを使う module.bind[FlightLookup] toInstance new FakeFlightLookup // ditto val doStuff = new DoStuffOnTheWeb("test", new Date())(module) doStuff.canFindMatchingFlight should be (true) // etc. } }
この例ではmodifyBindingsはイミュータブルなバインディングモジュールのコピーを返すが、そのミュータブルなカタチを使って、テストのための任意のバインディング変更を行うことができる。
ここでは、'maxThreadPoolSizeで識別されるInt値をアンバインドし(Noneにバインド)、 WebSearchとFlightLookup(どこかよそで定義されていると仮定)の二つをリバインドする。
モックを使うのもよい選択かもしれない(私はborachioを使っており、subcutと大変に相性がよい)が、ともあれここでもポイントはわかるだろう。
さて、新しいDoStuffOnTheWebの呼び出しに際し、テスト中に定義したミュータブルなモジュールを供給することができる。 そして、DoStuffOnTheWebインスタンスを機転とすう新規オブジェクトのチェインでそれが伝達されていく。
DoStuffOnTheWebインスタンス以下にある、いかなる深さの場所においてもWebSearchの使用は実際のものの代わりに、フェイクサービスに置き換えられる。が、システムの他の部分には影響しない。 modifyBindingsの度に新たなコピーが作成されるので、他のテストのリバインディングにようクロスコンフィギュレーション汚染を避けることができ、並行してテストを行うことができる。 テストの最後で一時的モジュールはリリースされ、必要であればGCされる。
そのほか
Subctには、DIとしてポピュラーなCakeパターンに似た特徴があるが、しかし任意の深さのバインディングコンフィギュレーションについて自在にコントロールできるというメリットがあり、そのうえファクトリ無しでインスタンス生成ができ、望むならコンストラクタパラメータを使うことも可能だ。これは注入やテスティング、特にファンクショナルテスティングに威力を発揮する(私の経験では、巨大なプロジェクトでCakeパターンを使うのはトリッキーであると思う)。
(訳注:その通りです。Cakeパターンは役たたずであり、価値は全くありません。これを紹介している方は、巨大で複雑なシステムを経験したことがないのです)。
しかしながら、Cakeパターンに比較して一つの特徴は抜け落ちてしまってはいる。 Cakeでは、コンパイル時に適切なバインディングが提供されているかどうかを検証することができるのだが、 subcutのようなダイナミックバインディングとルックアップという生来の性質を持つものでは、コンパイルはできているものの、実行時にバインディングが存在しないという事態が起こりうるし、それを検出するのは難しい。
このリスクを減らす二つの方法としてはこうだ。
injectIfBoundを使い、いつでもデフォルトの実装を提供すること。リーダビリティも向上するし、BindingExceptionが発生することはなくなる。
- injectIfBoundを使うという前提で、すべてのインスタンスを空のbindingModuleでテストしてみること。Test all of your instances with an empty bindingModule assuming you do use injectIfBound, or alternatively bind your standard configuration module (or each module in turn) implicitly into scope, and then create new instances of your injectable classes. This will enable you to pick up any missing or misconfigured bindings at testing time, rather than in production.
公平を期して言えば、Cakeはコンパイラがこのレベルの安全性を提供することのできる唯一のDIアプローチであると言うことができる。多くの(というかすべての)他のDIアプローチはバインディングが提供されなければランタイム障害が起こってしまう。 私は、このリスクを回避しようと努力はしたが、subcutはこの点では珍しくはない。
現在Locus Developmentにおいてsubcutを使用しているが、我々にとってはうまく動いている。 Locus Developmentが、この開発を忍耐強く待っていてくれたこと、テスティンググラウンドを提供してくれたことに感謝する。
ただ、subcutは現在pre-alpha(0.8)の状態である。我々には良いものの、あなたの子供を食べてしまうかもしれないし、サーバを吹き飛ばしたり、他のひどいことが起こることがおこるかもしれないが、私は責任はとれない。 しかし、上のような現象が起きたら(単に全く動作しない場合も)バグレポートは歓迎する。
APIを安定させようと努力はするが、「1.0より前」というタグは、何ヶ月か後の1.0リリースでは、我々があなたのコードを壊してしまう可能性のあることをも意味している。
Thanks, and Happy SubCutting.