Scala: DynamicVariable を利用した DI
Dependency Injection (DI/依存性の注入) とは
DI の種類 (例)
- コンストラクタ DI
- インタフェース DI
- setter DI
Scala における DI
いくつかの手法があり、多くの議論を生み出してきた。
- application.conf による書き換え
- Cake パターン
- 構造的部分型
- implicit parameter
- Reader モナド
- マクロによる書き換え
- DI フレームワーク
参考
- 実戦での Scala: Cake パターンを用いた Dependency Injection (DI) | eed3si9n
- Play 2.4 と Dependency Injection - tototoshi の日記
- Scrap Your Cake Pattern Boilerplate: Dependency Injection Using the Reader Monad - Originate Developer Blog
- [Scala]implicit parameterを使ったDI - Qiita
モチベーション
- ユニットテストの際に、外部サービスをモックに切り替えたい
- そんなに複雑な依存関係はない
- 「インジェクトしたいコンポーネントの利用者」の利用者のコードは変えたくない
- 実行時効率も考える
- 無駄な初期化が何度も行われるのは避ける
- 学習コストは小さいほうがいい
- DI のための外部ライブラリは使わない方向で
- 初見でも理解できるくらいシンプルに
- "Explicit is better than implicit."
DynamicVariable とは
あまり脚光を浴びることはない(?)が、標準で Scala に組み込まれているクラス。
- Scala Standard Library 2.11.6 - scala.util.DynamicVariable
- Console でも使われている: scala/Console.scala at v2.11.6 · scala/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 件のコメント:
コメントを投稿