ScalaでDIを行う方法はいろいろありますが、最近お気に入りのDIのやりかたの紹介です。
implicit defと、implicit parameterを組み合わせて必要なコンポーネントの受け渡しをコンパイラーに任せてしまいます。
利点
- Scalaの標準機能のみで簡単に実装できてライブラリ不要
- 全てコードで記述できるので、XML地獄や新しいDSLを覚える必要がない
- 使いたいコンポーネントをコンストラクタに追加するだけで使えて楽
欠点
- コンストラクターDIしか出来ない
手順
- インジェクションしたいコンポーネントをimplicit defで列挙したクラス(トレイト)を作る
- コンポーネントを使うクラスで、使いたいコンポーネントをimplicit parameterで定義
- コンポーネントを使うクラスを生成する際にコンポーネントを列挙したクラスのフィールドをimportする
のが、実装の大まかな流れになります。
サンプルコード
では、早速サンプルコードを。
基本部分
Repositories.scala
trait Repositories{
// DIしたいコンポーネントをimplicit defで列挙
implicit def userRepo : UserRepository
implicit def mailService : MailService
}
UserRepository.scala
// DIで渡したいコンポーネントの定義
trait UserRepository{
def getUser(id : Long) : Option[UserDAO]
}
case class UserDAO(id : Long){
//...implements
}
UserFacade.scala
// 使用したいコンポーネントを、コンストラクタのimplicit parameterに指定する
class UserFacade()(implicit userRepo : UserRepository){
def changeNickname(id : Long,nickname : String) : Optino[UserDAO]= {
userRepo.getUser(id).map(_.setAndSaveNickname(nickname))
}
}
Factory.scala
class Factory(repos : Repositories){
// コンポーネントを列挙したクラスのフィールドをimportしてやる
import repos._
lazy val userFacade = new UserFacade() // 勝手に必要なコンポーネントが渡される
}
使い方
Production.scala
object RepositoriesForProduction extends Repositories{
// lazy valにしておくと、不要なコンポーネントが初期化されない
lazy val userRepo = new TrueImplementedUserRepository()
lazy val mailService = new TrueImplmentedMailService()
}
val factory = new Factory(RepositoriesForProduction )
val userFacade = factory.userFacade
userFacade.changeNickname(1,"クドリャフカ")
Test.scala
object RepositoriesForTest extends Repositories{
// テスト用の場合は、mock化したコンポーネント
lazy val userRepo = mock[UserRepository]
lazy val mailService = mock[MailService]
}
// 初期化の方法は変わらず
val factory = new Factory(RepositoriesForTest)
val userFacade = factory.userFacade
userFacade.changeNickname(1,"クドリャフカ")
といった感じになります。
補足
あと、先ほど利点で挙げた「使いたいコンポーネントをコンストラクタに書くだけでいい」という事を補足しておきます。
例えば、「パスワードリセットして通知メールを送る」機能を追加するとします。
その場合、UserFacade.scalaだけを次のように修正します。
UserFacade.scala
// メール機能を使いたいのでMailServiceを追加
class UserFacade()(implicit userRepo : UserRepository,mailService : MailService){
def changeNickname(id : Long,nickname : String) : Optino[UserDAO]= {
userRepo.getUser(id).map(_.setAndSaveNickname(nickname))
}
def resetPassword() = {
userRepo.getUser(id).foreach( user => {
val newPassword = user.resetPassword() // resetPassword()は実装済みとする
emailService.sendTo(user,"パスワードリセット",s"新しいパスワードは${newPassword}です")
})
}
}
これだけで初期化部分の修正なしに、UserFacadeでEmailServiceを使えるようになります。