Posted at

ScalaのDIにMacWireを使う

More than 3 years have passed since last update.

ScalaでのDIはいくつもやり方があります。CakePatternを使った方法や、ImplicitParameterやReaderMonadを使った方法などです。javaっぽくGoogleGuiceみたいなDIコンテナを使うこともできます。

今回は数あるScalaのDIライブラリの中でも、コンストラクタDIを簡単に書けるMacWireというものを紹介します。


MacWireとは

詳細な説明はドキュメントがあります。

簡単に言うと、


  • コンストラクタDI最高。

  • コンパイル時に依存はチェックしたい。

  • 依存関係書くの辛いからマクロで解決。

という感じで、マクロでDIの依存関係を解決してくれます。


MacWireなしのコンストラクタDIの例


MacWireなし

trait DbModule {

lazy val db = new DB("localhost", 3306)
lazy val userDao = new UserDao(db)
}


MacWireありのコンストラクタDIの例


MacWireあり

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にしてみます。


Mockに

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はとても便利なものになります。


MacWireなし

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で書くと


MacWireあり

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に。


MacWireあり。テスト時

trait DbModuleTest extends DbModule {

override lazy val db = mock[DB]
override lazy val kvs = mock[Kvs]
}

このようにシンプルなので学習コストはないに等しく、簡単にコンストラクタDIができ最高ですね。


テストの時だけFutureを同期実行する

テストの時だけFutureは同期的にしたいことは良くあります。やり方は、overrideや抽象的なFuture等いくつかあると思います。

DIを使ってテストの時だけ同期実行にしてみます。

まず非同期に実行するためのクラスを定義します。


Async

class Async {

def apply[A](f: => A)(implicit ec: ExecutionContext): Future[A] = Future(f)
}

それをDIして使います。


UserService

import ExecutionContext...

class UserService(userDao: UserDao, async: Async) {
def insert(name: String): Future[Long] = async {
userDao.insert(name)
}
}



DbModule

trait DbModule {

lazy val db = new DB("localhost", 3306)
lazy val async = wire[Async]
lazy val userDao = wire[UserDao]
lazy val userService = wire[UserService]
}

テストの時は同期的にします。


DbModuleTest

trait DbModuleTest extends DbModule {

override lazy val db = mock[DB]
override lazy val async = new Async {
override def apply(f) = Future.successul(f) // テスト時だけ同期的にする
}
}

こうすれば、asyncを使うように統一することでテスト時は全部同期実行にできます。


おまけ

https://github.com/adamw/macwire

下のほうに載ってますが、Tagged typeに対応したり、Playでの使用例が書いてあったり、Implicitでもwireできたりしますね。

いくつかサンプルプロジェクトあったりもして、ドキュメントが充実してて良いですね。

おわり(´ο`)=3