subcut/start

subcut/Getting startedの翻訳

2011/7/1現在のhttps://github.com/dickwall/subcut/blob/master/GettingStarted.mdの翻訳。

このドキュメントではsubcutのはじめ方を説明する。ライブラリのダウンロード、最初のバインディングモジュールのセットアップ、注入可能なクラスの作り方に、implicitメソッドを使って注入可能クラスに設定を伝える(訳注:?)方法。ここではsubcutの推奨される使い方に注力する。簡単に言えば、

他にもsubcutを利用する方法はたくさんある。明示的な注入、コンストラクタ注入、mixinコンフィギュレーション、あるいはミュータブルなバインディングモジュールである(これを使用するのは非常な注意が必要)。

簡単な説明

これはハイレベルな外観であり、subcutを使うチートシートとも言える。 詳細についてはもっと読み進んでほしい。

これは、機能させるための単なる一つレシピであり、私のおすすめでもある。 subcutを使う方法は他にもある。

    object SomeConfigurationModule extends NewBindingModule({ module =>
      module.bind[X] toInstance Y
      module.bind[Z] toProvider { codeToGetInstanceOfZ() }
    })

    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))
        // ...
      }
    }

定義についていくつか注意

    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のコンストラクタパラメータとして追加する。

他ライブラリとの統合

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にバインド)、 WebSearchFlightLookup(どこかよそで定義されていると仮定)の二つをリバインドする。

モックを使うのもよい選択かもしれない(私はborachioを使っており、subcutと大変に相性がよい)が、ともあれここでもポイントはわかるだろう。

さて、新しいDoStuffOnTheWebの呼び出しに際し、テスト中に定義したミュータブルなモジュールを供給することができる。 そして、DoStuffOnTheWebインスタンスを機転とすう新規オブジェクトのチェインでそれが伝達されていく。

DoStuffOnTheWebインスタンス以下にある、いかなる深さの場所においてもWebSearchの使用は実際のものの代わりに、フェイクサービスに置き換えられる。が、システムの他の部分には影響しない。 modifyBindingsの度に新たなコピーが作成されるので、他のテストのリバインディングにようクロスコンフィギュレーション汚染を避けることができ、並行してテストを行うことができる。 テストの最後で一時的モジュールはリリースされ、必要であればGCされる。

そのほか

Subctには、DIとしてポピュラーなCakeパターンに似た特徴があるが、しかし任意の深さのバインディングコンフィギュレーションについて自在にコントロールできるというメリットがあり、そのうえファクトリ無しでインスタンス生成ができ、望むならコンストラクタパラメータを使うことも可能だ。これは注入やテスティング、特にファンクショナルテスティングに威力を発揮する(私の経験では、巨大なプロジェクトでCakeパターンを使うのはトリッキーであると思う)。

(訳注:その通りです。Cakeパターンは役たたずであり、価値は全くありません。これを紹介している方は、巨大で複雑なシステムを経験したことがないのです)。

しかしながら、Cakeパターンに比較して一つの特徴は抜け落ちてしまってはいる。 Cakeでは、コンパイル時に適切なバインディングが提供されているかどうかを検証することができるのだが、 subcutのようなダイナミックバインディングとルックアップという生来の性質を持つものでは、コンパイルはできているものの、実行時にバインディングが存在しないという事態が起こりうるし、それを検出するのは難しい。

このリスクを減らす二つの方法としてはこうだ。

公平を期して言えば、Cakeはコンパイラがこのレベルの安全性を提供することのできる唯一のDIアプローチであると言うことができる。多くの(というかすべての)他のDIアプローチはバインディングが提供されなければランタイム障害が起こってしまう。 私は、このリスクを回避しようと努力はしたが、subcutはこの点では珍しくはない。

現在Locus Developmentにおいてsubcutを使用しているが、我々にとってはうまく動いている。 Locus Developmentが、この開発を忍耐強く待っていてくれたこと、テスティンググラウンドを提供してくれたことに感謝する。

ただ、subcutは現在pre-alpha(0.8)の状態である。我々には良いものの、あなたの子供を食べてしまうかもしれないし、サーバを吹き飛ばしたり、他のひどいことが起こることがおこるかもしれないが、私は責任はとれない。 しかし、上のような現象が起きたら(単に全く動作しない場合も)バグレポートは歓迎する。

APIを安定させようと努力はするが、「1.0より前」というタグは、何ヶ月か後の1.0リリースでは、我々があなたのコードを壊してしまう可能性のあることをも意味している。

Thanks, and Happy SubCutting.

last edited 2011-07-03 09:19:46 by ysugimura