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 件のコメント:
コメントを投稿