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 は適当に実装されているものとする。

your/company/cache/CacheService.scala
1
2
3
trait CacheService {
  def get(key: String): String
}
your/company/cache/RealCache.scala
1
2
3
object RealCache extends CacheService {
  def get(key: String): String = ???
}
your/company/cache/MockCache.scala
1
2
3
object MockCache extends CacheService {
  def get(key: String): String = ???
}

 

コンポーネント利用者

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

your/company/image/ImageCache.scala
1
2
3
class ImageCache(implicit cacheService: CacheService) {
  def read(key: String): String = cacheService.get(key)
}

 

コンポーネントの管理

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

your/company/inject/Repositories.scala
1
2
3
4
5
6
7
8
9
10
11
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)
your/company/inject/Factory.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}

 

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

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

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

 

ユニットテストを行う

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

1
2
3
4
5
6
7
"ImageCache#read" should {
  "be xxx" in {
    Factory.withMock {
      Factory.imageCache.read("abc") must_== "xxx"
    }
  }
}

 

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

0 件のコメント:

コメントを投稿