Locked History Actions

Diff for "subcut/start"

Differences between revisions 8 and 9
Deletions are marked like this. Additions are marked like this.
Line 254: Line 254:
SubCut provides many features similar to the cake pattern popular for dependency injection, but has the advantage of providing full control over binding configuration at any depth, while still being able to create new instances without factories, and using constructor parameters if you desire. It is great for injection and testing, particularly functional testing (which can in my experience get tricky in large projects using the cake pattern).

It has one major feature missing when compared with cake though. With Cake, the compiler can always tell if a suitable binding has been provided at compile time, while because of the nature of the dynamic binding and lookup used in subcut, sometimes although the program compiles, a binding may be missing at runtime and this can be hard to detect.

There are two main ways of reducing the risk of this occuring:

 * Use injectIfBound and always supply default implementations, which is better for readability anyway, and means that there will never be a BindingException thrown at runtime, and

 * 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.
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.

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を追加すればよい。

The BindingModule implicit must be called bindingModule, as that is the value that the Injectable trait will look for. If bindingModule is not defined, you will get a compile time error. In this way subcut helps you to remember to include your injection bindings in an unbroken chain down to the lowest level class that needs it.

  • The Injectable trait must appear in the traits for the injectable class somewhere. This defines all of the injection methods like 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 will look to see if there is a definition bound to trait Session with id 'currentUser, and if so it will use that (in this case there is, so Session.getCurrent() will not be evaluated or used). However, for the line:

    val flightLookup = injectIfBound[FlightLookup] { new OrbitzFlightLookup }

subcut looks for the trait FlightLookup (with no extra ID name to identify it) and doesn't find one bound, so instead falls back to the default expresion to the right, which is evaluated and results in a new instance of OrbitzFlightLookup every time a new instance of our class is created. OrbitzFlightLookup can itself be an injected class, and because the implicit parameter for bindings is in scope, the compiler will apply those bindings to it automatically for us, so OrbitzFlightLookup could be defined as:

Note that OrbitzFlightLookup must mix in the FlightLookupTrait in order to satisfy the binding, and must still include the implicit parameter list even though it doesn't have any constructor parameters.

  • The rest of the class can use these injected values as normal through the methods defined on the traits they are defined under. You can also inject values at any point in the class, not just in the constructor code, and any new Injectable instances you create at any point in the class will get the binding configuration passed into them automatically unless you explicitly override them.

injectIfBound is only one form of injection subcut provides, the others being inject[Trait] which will always inject the trait from the bindingModule definition, and will fail if no such binding is provided. The other form is injectIfMissing[Trait] where the instance is only bound in if it has not already been provided in a constructor parameter (see the scaladoc for more information on how to use this). Both of these other types of injection will fail if no binding has been provided, while injectIfBound will always fall back on the default if no binding is available. This is one of the reasons it is the recommended way to use subcut, since you reduce the possibility of runtime binding failure. Another reason is that programmers reading your code can easily see what the "normal" implementation of a specific trait is, right there in the class definition.

Using injectIfBound also makes it possible for you to have a completely empty BindingModule for production, in fact I often do. 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.

Using your 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.

To be fair, Cake is the only DI approach I have ever seen where the compiler can provide that level of safety. Most (all) other DI approaches I have seen can get runtime failures if required bindings are unavailable, so subcut is not all that unusual in this regard, although I have tried hard to provide strategies for working around the risk in practice.

Finally we have been using SubCut for some time now interally at Locus Development, and it works well for us. I would like to thank Locus Development for their patience and support while I developed SubCut, and also for providing a testing ground for it. I will also remind you that SubCut is currently in pre-alpha (0.8) and while it works great for us, it might eat your children, blow up your servers and other horrible things that I am not responsible for. I would however like bug reports if it does do any of the above (or just plain doesn't work for some reason). The pre 1.0 tag should also serve as a reminder that while I will try and keep the API stable, there could well be changes that will break your code in the months ahead as we approach a stable 1.0 release.

Thanks, and Happy SubCutting.