Revision 12 as of 2011-07-02 03:24:38

Clear message
Locked History Actions

subcut/start

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のただ一つのインスタンスにバインドされる。 このバインディングでは、いつも同じインスタンスが返されるので、スレッド化環境ではこの単一のインスタンスがスレッドセーフでなくてはいけない。

webAnalyzeという識別子を付加されたAnalyzerは、WebAnalyzerにバインドされ、それが要求される度にWebAnalyzerの新たなインスタンスがリフレクションを通して生成されて提供される。WebAnalyzerは引数0の(デフォルトの)コンストラクタを持たなければならないことに注意。

currentUserと名付けられたSesionは関数にバインドされ、バインドの使用ごとにその関数が呼び出されてSessionを提供する。 これはインスタンス生成の最も柔軟性のあるやり方である。なぜなら、プロバイダメソッドは、それが返すインスタンスがSessionトレイトのサブタイプである限り、いかなることをもすることができるからである。

maxThreadPoolSizeと識別されるIntは常に10というInt値を返す。 IntやStringのような共通タイプを識別名無しでバインドすることはおすすめできない。 そのバインディングが非常に広範囲になってしまい、知らないうちに値をピックしてしまうことがあるからだ。

最後のWebSearchは、供給メソッドで生成される単一のインスタンスにlazilyにバインドされる。 これについては二つの注意事項がある。 toLazyInstanceは、それが最初にバインドされるまで生成を引き伸ばすが、その後は同じインスタンスを常に返す。 lazyバインディングは

The lazy binding is necessary in this case for the second reason - a specific binding configuration is provided to the GoogleSearchService constructor in the form of a second curried parameter. It is necessary for this to be included as there is no implicit binding that can be picked up in scope within the binding module configuration, and the laziness is required to avoid using the configuration module before it has been defined. If this is confusing to you, don't worry about it until you have read and understood the implicit binding approach (described below) to providing configuration, and then it should make more sense.

ここでのバインディングモジュールはシングルトンオブジェクトであることに注意。 これが物事をナイスでシンプルな状態に保つおすすめの方法である。 あなたのプロジェクトのどこかのパッケージでこのような定義を行い、そこでコンフィギュレーションを示し、探しやすくしておくこと。

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のようなすべての注入メソッドが定義されている。
  • The injectIfBound[Trait] vals defined at the top of the class are where the configuration bindings are used. injectIfBound will use the configured definition if one is provided, and if not, it will fall back to the provided default on the right hand side of the expression, so for example, in the line:

    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のまっとうな使い方であるというものだ)。

  • You must provide the binding module still, so that it may be overridden for testing or other purposes, but leaving the BindingModule empty means that the defaults will always be used, and also carries a slight performance advantage if you do so, since if the bindingModule is empty, the lookup is optimized out when binding. You can still override bindings at any time to change the default behavior.

あなたのBindingModuleを使う

So far we have created a binding module, and shown how to inject bindings into traits. The last piece is to connect the dots.

With a BindingModule provided via the implicit definition (implicit val bindingModule: BindingModule) to an Injectable class, in order to use a specific module you must do one of two things:

Either, create an implicit value definition before you create the new instance of the top class, like this:

    implicit val bindingModule = ProjectConfiguration
    val topInstance = new DoStuffOnTheWeb("stuff", new Date())

in which case the binding module will be provided to the DoStuffOnTheWeb automatically, and to all instances created inside of that as well (this is how you provide a project wide configuration with a single assignment). Alternatively you could use the explicit (shorthand form) which is:

    val topInstance = new DoStuffOnTheWeb("stuff", new Date())(ProjectConfiguration)

The explicit is only needed for the first instance, as it is implicitly available to all instances under that (it is defined as an implicit in the parameter list - that makes it implicit in the class scope). The shorthand form does exactly the same as the implicit val form above, but it's just less typing. This is also how you can override the implicit binding at any depth in the tree (just provide it explicitly) and also how you can specify an explicit binding for a new object while creating a new binding module (use the binding module you are defining, and make the binding lazy or a provider to avoid using the module before it is ready).

You can break the chain accidentally, but if you do the compiler will give an error. To illustrate this:

    Class A is Injectable with implicit binding
    Class B is not Injectable
    Class C is Injectable with implicit binding
    Class D is not Injectable

if top level instance of Class A creates a new Class B, it can do so just fine. It can also create new instances of class C and D without problem.

However, class B cannot create an instance of Class C without explicitly providing some kind of configuration. Class B did not get an implicit binding module (because it does not have the implicit binding in its parameter list), therefore there is no implicit binding available when it tries to create a new instance of class C. B can create an instance of class D just fine, since it doesn't need a binding module either. In other words, the chain must remain unbroken as far down as you use the implicit, but need go no further (at some point you will likely be dealing with small leaf classes that don't themselves need anything injected nor use classes that do - at that point you can skip adding Injectable and the implicit to each class).

The compiler will not compile in the above situation. Instead you will get a compiler error when trying to create a new instance of class C in class B, since no value for the required bindingModule is provided. Thus the compiler will help you make sure the chain is intact as far as it needs to go.

To correct the problem, simply add the implicit val bindingModule: BindingModule to a curried constructor parameter in Class 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.

Integrating with other libraries

SubCut can easily be integrated with other libraries that are not subcut aware by providing the bindingModule configuration by a couple of different mechanisms. This includes libraries like, for example, wicket, where you do not control the creation of new page instances.

The traditional problem is that if you don't control the new instance, how do you get the right binding configuration to the top level instance created by the library. Normally the library needs some kind of integration plugin to help provide the right configuration.

In subcut there is an easier way, simply create a new subclass of the Injectable class, and provide a definition for the bindingModule that is implicit in the constructor of that subClass, e.g.

    class SomePage(implicit val bindingModule: BindingModule) extends WicketPage with Injectable { }

    class ProdSomePage extends SomePage(ProjectConfiguration)

You can now register ProdSomePage with the wicket library to be created when needed. It will always be bound to the same configuration, but that's normally what you want in production anyway. Testing with SubCut

At this point it should be clear that you can easily provide your own custom bindings for any new Injectable instance, and those bindings will be used from that point down in the new instances hierarchy. There are some enhancements provided for convenience beyond this for testing purposes however, since this is such a common place to want to change bindings.

A typical test with SubCut overriding will look like this:

    test("Test lookup with mocked out services") {
      ProjectConfiguration.modifyBindings { module =>
        // module now holds a mutable copy of the general bindings, which we can re-bind however we want
        module.bind[Int] identifiedBy 'maxThreadPoolSize to None    // unbind and use the default
        module.bind[WebSearch] toInstance new FakeWebSearchService  // use a fake service defined elsewhere
        module.bind[FlightLookup] toInstance new FakeFlightLookup   // ditto

        val doStuff = new DoStuffOnTheWeb("test", new Date())(module)

        doStuff.canFindMatchingFlight should be (true)
        // etc.
      }
    }

In this example, the modifyBindings hands us back a copy of the immutable binding module, but in a mutable form in which we can change any bindings we like for the tests. In this case we unbind (bind to None) the Int identified by 'maxThreadPoolSize, and rebind both the WebSearch and FlightLookup traits to fake ones (which we assume have been defined elsewhere). Mocks would be a better choice here (I use borachio, and it works great with subcut) but you get the point. Now, when we call new DoStuffOnTheWeb, we provide the mutable module available in the test, and that gets passed down the chain for new objects starting with the DoStuffOnTheWeb instance. Any use of WebSearch injection at any depth under this instance of DoStuffOnTheWeb will get the fake service instead of the real one, but the rest of the system will be unaffected. A new copy module is created every time we do modifyBindings, so you can test in parallel without any cross configuration polution from rebindings in other tests. At the end of the test, the temporary module is released and can be garbage collected when necessary.

Other Notes

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.