Scala
Scala.js
AIrframe

Scala用DIコンテナ airframeの使い方

ScalaでDIコンテナというとGoogle Guiceを使っている人が多いと思うけど、僕は最近airframeを使い始めました。

https://github.com/wvlet/airframe

何がどう違うのというのは以下の記事を読んで欲しい。個人的には、Scalaで使うための設計が前提になっているかが大きいと思っています。Guiceでもいいけど、無邪気に使っているとJava前提のライブラリはハマることもあるし、Scalaの独自機能に対するケアもないので…。

https://wvlet.org/airframe/docs/comparison.html

使い方はQuick Startを読んでください。たけぞえさんのブログ記事 「Scala用のDIライブラリAirframeを試してみた」 でも紹介されています。1

以上、終了でもよかったのですが、日本語で簡単に解説しておきます。

最低限知っておくとよいのは、Design, Sessionです。あとは、DIコンテナがサポートするLife Cycleはドキュメントをみてください。DesignとSessionだけ、公式ドキュメントのコードを例に説明します。


コンストラクタ・インジェクション

インジェクションのパターンには、以下の二つがあります。


  • コンストラクタ・インジェクション

  • イン・トレイト・インジェクション

以下はコンストラクタ・インジェクションの例です。アノテーションは不要です。

import wvlet.airframe._

class MyApp(val config:AppConfig)
case class AppConfig(appName:String)

イン・トレイト・インジェクションは以下のようにtraitのフィールドに対するインジェクションですが、具体的には最後に紹介します。

class Database(name:String, conn:Connection)

trait DatabaseService {
val db = bind[Database]
}


Design

GuiceのAbstractModule#configure内で記述するバインディングはDesignを使うとできます。newDesignの後に続けてbindとかtoInstanceとか、GuiceのDSLと似ています。戻り値もDesignになるので、状況に応じてDI設定を追加したり削除したりできます。

Design#bind[A].の後続で使えるメソッドはGuice同様様々あります。詳しくはドキュメントを参照してください。

// Define a design

val d: Dession = newDesign
.bind[AppConfig].toInstance(AppConfig("Hello Airframe!"))


Session

GuiceのInjectorに相当するのがSessionです2。Sessionを使うには以下が必要です。


  • Sessionがなければ Design#newSessionで生成する

  • Sessionを開始するために、Session#startを呼ぶ

  • Sessionを停止するために、Session#shutdownを呼ぶ

つまり、Sessionの生成・開始・停止が必要になります。当然、これらのマニュアル操作を行ってもよいですが、以下の便利な高階メソッドで自動化できます。3


  • Design#build[A](body: A => Any): Any

  • Design#withSession[U](body: Session => U): U

// Create MyApp. AppConfig in the design will be used

d.build[MyApp]{ app => // new MyApp(AppConfig("Hello Airframe!"))
// Do something with app
// e.g.) println(app.config.appName)
}
// Session will be closed here

val appName = d.withSession{ session =>

session.build[MyApp].config.appName
}


イン・トレイト・インジェクション

最後にイン・トレイト・インジェクションを説明します。traitが持つフィールドを初期化する際にbindを使うだけです。クラスでは使えないので注意。

import wvlet.airframe._

class Database(name:String, conn:Connection)
trait DatabaseService {
val db = bind[Database]
}

val d = newDesign
.bind[Connection].to[ConnectionImpl]

d.withSession { session =>
// Creates a new DatabaseService with ConnectionImpl
val service = session.build[DatabaseService]
}

// [DON'T DO THIS] You cannot use bind[X] inside classes:
class A {
val a = bind[B] // [Error] Because class A can't find the current session
}


まとめ

airframeは、Guiceの知識があれば簡単に始められて、Guice以上にシンプルにコードが書けます。ここでは紹介しなかったのですが、タイプエリアスやジェネリック(高階型も対応)のサポートもあります。お勧めです。

次は、airframeを使った、高階型を持つ型のDIについて書きます。

追記: 書きました → 高階型の型パラメータを持つDDDリポジトリをDIできるようにする





  1. AccountControllerでbindを使ってますが、多分これはコンストラクタ・インジェクションにするか、classをtraitに変えないとDIが失敗するかも。 



  2. 厳密には責務が違うので相当するといってはいけない気がするが、Guice脳にもわかりやすく…。 



  3. Sessionをfor式で合成できるようにReader化するものいいですね。type SessionRedader[A] = Reader[Session, A]とか