Scala

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