目次

Version 2, last updated by sorenbs at Nov 27 18:17 UTC

Lift と依存性注入

依存性注入は Java の世界では重要なトピックです。なぜなら、Java には抽象的なインタフェースを具体的な実装にバインドする基本的な機能 (関数など) がいくつか欠けているからです。実際には、MyInterface thing = new MyInterfaceImpl() とする方がはるかに簡単なので、ほとんどの開発者はこの方法を使います。

Scala の cake pattern を使うと、開発者は Scala のトレイトを組み合わせて複雑なふるまいを実現することができます。Jonas Bonér が依存性注入についてたいへん優れた記事を書いているので、参照してください。

cake pattern は、Java 開発者に完全な依存性注入機能を与えるわけではなく、cake pattern が役に立つのは半分までです。cake pattern を使えば、Scala のトレイトから複雑なクラスを作ることができますが、ある与えられた状況下で提供する cake の組み合わせについて動的な選択肢を用意するという意味では、cake pattern はあまり役に立ちません。Lift には、依存性注入のパズルを完成させるための追加の機能が用意されています。

Lift ライブラリと Injector

Lift は Web フレームワークであると同時に、Scala ライブラリの集合でもあります。Lift の commonactorjson、および util パッケージは、Scala 開発者がアプリケーションをビルドするための一般的なライブラリを提供しています。Lift のライブラリはよくテストされていて、広く使われており、サポートも良好で、きちんとしたスケジュールに沿ってリリースされています (月ごとのマイルストーン、3 か月ごとのリリース)。

Lift の Injector トレイトは依存性注入の基礎となるトレイトです。

/**
 * A trait that does basic dependency injection.
 */
trait Injector {
  implicit def inject[T](implicit man: Manifest[T]): Box[T]
}

このトレイトを使用するには、次のようにします。

object MyInjector extends Injector {...}

val myThing: Box[Thing] = MyInjector.inject

MyThing のインスタンスが Box に入っているのは、MyInjectorThing のインスタンスの作成方法を知っていることが保証されないからです。

Lift には、SimpleInjector と呼ばれる Injector の実装が用意されています。SimpleInjector を使うと、注入を目的として関数を登録 (および再登録) することができます。

object MyInjector extends SimpleInjector

def buildOne(): Thing = if (testMode) new Thing with TestThingy {} else new Thing with RuntimeThingy {}

MyInjector.registerInjection(buildOne _) // Thing を作成する関数を登録

val myThing: Box[Thing] = MyInjector.inject

悪くないやり方ではないでしょうか。この方法を使えば、注入時に決定を行う関数を定義することができ、実行時 (またはテスト時) に関数を変更することができます。ただし、2 つ問題があります。注入のたびに Box を取得するのはベストな方法ではありません。また、グローバルスコープの関数を使うということは、その関数にロジック (テスト vs. 実働 vs. xxx) もたくさん詰め込む必要あります。SimpleInjector では、このような場合に役に立つさまざまな方法を提供しています。

object MyInjector extends SimpleInjector {
val thing = new Inject(buildOne _) {} // thing を定義します.すぐに評価、登録されるためには val である必要があります.
}

def buildOne(): Thing = if (testMode) new Thing with TestThingy {} else new Thing with RuntimeThingy {}

val myThingBox: Box[Thing] = MyInjector.inject

val myThing = MyInjector.thing.vend // Thing のインスタンスを提供.

Inject には実はまだ便利な機能があります。Inject を使うと、関数のスコープを限定することができます。これはテスト時や、特定の呼び出しのスコープで動作を変更する必要がある場合などに便利です。

MyInjector.thing.doWith(new Thing with SpecialThing {}) {
val t = MyInjector.thing.vend // SpecialThing のインスタンス
val bt: Box[Thing] = MyInjector.inject // Full(SpecialThing)
}

MyInjector.thing.default.set(() => new Thing with YetAnotherThing {}) // グローバルスコープを設定.

doWith 呼び出しのスコープ内では、MyInjector.thingSpecialThing のインスタンスを提供します。これは、テスト時や、呼び出しのスコープ内またはグローバルに動作を変更したい場合に便利です。この方法を使えば、Java の依存性注入パッケージで利用できる機能のほとんどを実現できます。ただ、Lift WebKit を使うと、処理はさらに簡単になります。

Lift WebKit と強化された注入スコーピング

Lift の WebKit には、HTTP リクエストを処理したり HTML 操作を行ったりするための幅広いツールが用意されています。

Lift WebKit の FactorySimpleInjector を継承していますが、それに加えて、現在の HTTP リクエストまたは現在のコンテナセッションに基づいて関数のスコープを限定する機能を持っています。

object MyInjector extends Factory {
 val thing = new FactoryMaker(buildOne _) {} // thing を定義します.すぐに評価、登録されるためには val である必要があります.
}
MyInjector.thing.session.set(new Thing with ThingForSession {}) // セッションの期間中に提供されるインスタンスを設定.
MyInjector.thing.request.set(new Thing with ThingForRequest {}) // リクエストの期間中に提供されるインスタンスを設定.

WebKit の LiftRulesFactory で、LiftRules に含まれる多くのプロパティは FactoryMakers です。これは、呼び出しのスコープで動作を変更できることを意味します (テスト時に便利です)。

LiftRules.convertToEntity.doWith(true) {
... いくつかの文字をエンティティに変換するテスト
}

現在のリクエストに基づく場合 (たとえば、現在のリクエストの間、docType の計算方法を変更できます):

if (isMobileReqest) LiftRules.docType.request.set((r: Req) => Full(DocType.xhtmlMobile))

現在のセッションに基づく場合 (たとえば、セッションが作成されたときにいくつかのルールに基づいて maxConcurrentRequests を変更するなど):

if (browserIsSomethingElse) LiftRules.maxConcurrentRequests.session.set((r: Req) => 32) // このセッションでは、32 の並行リクエストを許可.

結論

Lift の SimpleInjector/Factory は、グローバル関数、コールスタック・スコーピング、リクエスト・スコーピング、およびセッション・スコーピングに基づいてインスタンスを提供するための強力で柔軟なしくみを用意しており、XML による設定やバイトコード書き換えのような方法に頼ることなく、多くの Java ベースの依存性注入フレームワークと比べてより柔軟性の高い機能を提供しています。