ScalaでのDIはいくつもやり方があります。CakePatternを使った方法や、ImplicitParameterやReaderMonadを使った方法などです。javaっぽくGoogleGuiceみたいなDIコンテナを使うこともできます。
今回は数あるScalaのDIライブラリの中でも、コンストラクタDIを簡単に書けるMacWireというものを紹介します。
MacWireとは
詳細な説明はドキュメントがあります。
簡単に言うと、
- コンストラクタDI最高。
- コンパイル時に依存はチェックしたい。
- 依存関係書くの辛いからマクロで解決。
という感じで、マクロでDIの依存関係を解決してくれます。
MacWireなしのコンストラクタDIの例
trait DbModule {
lazy val db = new DB("localhost", 3306)
lazy val userDao = new UserDao(db)
}
MacWireありのコンストラクタDIの例
import com.softwaremill.macwire._
trait DbModule {
lazy val db = new DB("localhost", 3306)
lazy val userDao = wire[UserDao]
}
テストの時は同じですが、↑のようにwireを使うことで依存を書く必要がなくなります。
実際はマクロでnew UserDao(db)
に展開されます。
使うときはmix-inして使えばokですね。
テスト用に、mockにしたModuleにしてみます。
trait DbModuleMock {
override lazy val db = mock[DB]
}
そしてテストでもmix-inしたりして使えばいいですね。
class UserDaoSpec extends DbModuleMock {
"UserDao" should {
"insert" it {
userDao.insert(1)
verify(db).insert(1) // mockになってるので呼ばれたかを確認してる
}
}
}
ここまでが簡単な例でした。
少し複雑な依存の例
依存が増えた時に、このwireはとても便利なものになります。
trait DbModule {
lazy val db = new DB("localhost", 3306)
lazy val userDao = new UserDao(db)
lazy val userIdGen = new UserIdGen()
lazy val kvs = new Kvs("localhost", 6379)
lazy val userCache = new UserCache(kvs)
lazy val userService = new UserService(userIdGen, userCache, userDao)
}
dbのアクセスをkvs経由にしてcacheするみたいなイメージです。
これはwireで書くと
trait DbModule {
lazy val db = new DB("localhost", 3306)
lazy val userDao = wire[UserDao]
lazy val userIdGen = wire[UserIdGen]
lazy val kvs = new Kvs("localhost", 6379)
lazy val userCache = wire[UserCache]
lazy val userService = wire[UserService]
}
とてもシンプルで良いですね。
テスト時はmockにしたModuleに。
trait DbModuleTest extends DbModule {
override lazy val db = mock[DB]
override lazy val kvs = mock[Kvs]
}
このようにシンプルなので学習コストはないに等しく、簡単にコンストラクタDIができ最高ですね。
テストの時だけFutureを同期実行する
テストの時だけFutureは同期的にしたいことは良くあります。やり方は、overrideや抽象的なFuture等いくつかあると思います。
DIを使ってテストの時だけ同期実行にしてみます。
まず非同期に実行するためのクラスを定義します。
class Async {
def apply[A](f: => A)(implicit ec: ExecutionContext): Future[A] = Future(f)
}
それをDIして使います。
import ExecutionContext...
class UserService(userDao: UserDao, async: Async) {
def insert(name: String): Future[Long] = async {
userDao.insert(name)
}
}
trait DbModule {
lazy val db = new DB("localhost", 3306)
lazy val async = wire[Async]
lazy val userDao = wire[UserDao]
lazy val userService = wire[UserService]
}
テストの時は同期的にします。
trait DbModuleTest extends DbModule {
override lazy val db = mock[DB]
override lazy val async = new Async {
override def apply(f) = Future.successul(f) // テスト時だけ同期的にする
}
}
こうすれば、asyncを使うように統一することでテスト時は全部同期実行にできます。
おまけ
下のほうに載ってますが、Tagged typeに対応したり、Playでの使用例が書いてあったり、Implicitでもwireできたりしますね。
いくつかサンプルプロジェクトあったりもして、ドキュメントが充実してて良いですね。
おわり(´ο`)=3