4.06.2015

Scala: Dependency Injection with DynamicVariable

Scala: DynamicVariable を利用した DI

Dependency Injection (DI/依存性の注入) とは

依存性の注入 - Wikipedia

DI の種類 (例)

  • コンストラクタ DI
  • インタフェース DI
  • setter DI

 

Scala における DI

いくつかの手法があり、多くの議論を生み出してきた。

 

参考

 

モチベーション

  • ユニットテストの際に、外部サービスをモックに切り替えたい
    • そんなに複雑な依存関係はない
  • 「インジェクトしたいコンポーネントの利用者」の利用者のコードは変えたくない
  • 実行時効率も考える
    • 無駄な初期化が何度も行われるのは避ける
  • 学習コストは小さいほうがいい

 

DynamicVariable とは

あまり脚光を浴びることはない(?)が、標準で Scala に組み込まれているクラス。

 

DynamicVariable を利用した DI の例

 

サンプル・アプリケーション

あるキャッシュサービスに対して、外部サービスとモックの切り替えを DI で行うケースを考える。

  • インジェクト対象: CacheService
  • CacheService の利用者: ImageCache
  • ImageCache を他のロジックが使用

といった構成とする。

アイデア
  • implicit parameter を使った コンストラクタ DI + Factory パターンがベース => ほぼ こちら の写経
  • Factory の状態を グローバルな DynamicVariable として保持し、目的に応じて Factory を切り替えて使う
    • 各コンポーネントの初期化処理は、利用する Factory の個数分だけで済むように

 

コード例

 

インジェクト対象

CacheService は適当に実装されているものとする。

trait CacheService {
  def get(key: String): String
}
object RealCache extends CacheService {
  def get(key: String): String = ???
}
object MockCache extends CacheService {
  def get(key: String): String = ???
}

 

コンポーネント利用者

implicit なコンストラクタ・パラメータを受け取るクラスを作る。

class ImageCache(implicit cacheService: CacheService) {
  def read(key: String): String = cacheService.get(key)
}

 

コンポーネントの管理

環境に応じたコンポーネントのセットを Repositories として定義する。

sealed trait Repositories {
  implicit def cacheService: CacheService
}

case object DefaultRepositories extends Repositories {
  lazy val cacheService = RealCache
}

case object MockRepositories extends Repositories {
  lazy val cacheService = MockCache
}

 

ファクトリの作成
  • Factory.currentVar に Factory クラスを継承したオブジェクトを DynamicVariable として保持する
  • withMock という関数を作り、一時的に環境を MockFactory に切り替えて処理を実行できるようにする
  • Factory.imageCache は毎回評価が行われるように、関数として定義する。(val や lazy val では NG)
sealed abstract class Factory(repos: Repositories) {
  import repos._

  lazy val imageCache = new ImageCache
}

// concrete factories
case object DefaultFactory extends Factory(DefaultRepositories)
case object MockFactory extends Factory(MockRepositories)

// global accessor
object Factory {
  private[this] val currentVar = new DynamicVariable[Factory](DefaultFactory)

  def withMock[T](thunk: => T) = currentVar.withValue(MockFactory)(thunk)

  def imageCache = currentVar.value.imageCache
}

 

コンポーネント利用クラスを使う

インスタンス生成は、必ずファクトリ経由で行うようにする。

trait Image {
  def readCache = Factory.imageCache.read("abc")
}

 

ユニットテストを行う

明示的に Factory.withMock を指定するので、処理の流れがわかりやすいと思う。
「コンポーネント利用クラス」を利用する処理も同じ書式で書けるのが嬉しい。

"ImageCache#read" should {
  "be xxx" in {
    Factory.withMock {
      Factory.imageCache.read("abc") must_== "xxx"
    }
  }
}

 

いずれもまだアイデア段階のものである。考慮漏れ and/or もっと良い方法があるかもしれない。

0 件のコメント:

コメントを投稿