Scala使用Reader Monad实现依赖注入

  有许多方式在Scala中无需框架实现依赖注入,Cake模式是最流行的一种方式,被Scala编译器自己也使用了,使用了隐式参数方式不太流行,在Scala并发包中有使用。

  Reader Monad实现依赖注入与这些流行的模式是有区别的,让我们通过一个案例比较看看它们的区别,这个案例是一个仓储repository如下,我们需要将其实现子类注入到使用这个仓储接口的任何代码中,这样,仓储可以是数据库实现,也可以是测试中一个模拟mock实现:

trait UserRepository {
  def get(id: Int): User
  def find(username: String): User
}

Cake模式

  cake模式是将依赖作为一个组件trait:

trait UserRepositoryComponent {

  def userRepository: UserRepository

  trait UserRepository {
    def get(id: Int): User
    def find(username: String): User
  }
}

  然后我们使用self-type在抽象类Users中申明依赖:

trait Users {

  this: UserRepositoryComponent =>

  def getUser(id: Int): User = {
    userRepository.get(id)
  }

  def findUser(username: String): User = {
    userRepository.find(username)
  }
}

  下面userInfo是继承抽象类Users:

trait UserInfo extends Users {

  this: UserRepositoryComponent =>

  def userEmail(id: Int): String = {
    getUser(id).email
  }

  def userInfo(username: String): Map[String, String] = {
    val user = findUser(username)
    val boss = getUser(user.supervisorId)
    Map(

      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
  }
}

  扩展依赖组件trait提供其一个实现:

trait UserRepositoryComponentImpl extends UserRepositoryComponent {

  def userRepository = new UserRepositoryImpl

  class UserRepositoryImpl extends UserRepository {

    def get(id: Int) = {
      ...
    }

    def find(username: String) = {
      ...
    }
  }
}

  最后,我们将核心实现创建带有依赖的实例:

object UserInfoImpl extends
  UserInfo with
  UserRepositoryComponentImpl

  测试可以混合一个Mock实现:

object TestUserInfo extends
  UserInfo with
  UserRepositoryComponent {

  lazy val userRepository = mock[UserRepository]

}

Implicits

  cake模式很好,但是需要很多模板,另外一个实现依赖注入的是增加隐式implicit参数到需要依赖的方法中:

trait Users {

  def getUser(id)(implicit userRepository: UserRepository) = {
    userRepository.get(id)
  }

  def findUser(username)(implicit userRepository: UserRepository) = {
    userRepository.find(username)
  }

}

  Users的实现子userInfo如下:

object UserInfo extends Users {

  def userEmail(id: Int)(implicit userRepository: UserRepository) = {
    getUser(id).email
  }

  def userInfo(username: String)(implicit userRepository: UserRepository) = {
    val user = findUser(username)
    val boss = getUser(user.supervisorId)

    Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"

    )
  }
}

  这只需要很少代码,允许我们使用不同的UserRepository,两种方式:通过定义自己的隐式值或作为显式参数传递。

  这个方式的问题是它将方法签名搞乱了,每个依赖UserRepository的方法,这个案例中是userEmail和userInfo还得定义一个隐式参数。

Reader Monad

  在Scala中,一个一元函数(只带有一个参数的函数)是类型Function1类型的对象,例如,我们能定义一个函数triple将Int作为参数:

val triple = (i: Int) => i * 3
triple(3)   // => 9

  triple的类型是Int=>Int ,它只是Function1[Int, Int]的另外一种表示方式。

  Function1让我们使用andThen从一个已经存在的函数中再创建一个新函数:

val thricePlus2 = triple andThen (i => i + 2)
thricePlus2(3)  // => 11

  andThen混合了两种一元函数到第三方函数:首先是应用第一个函数,然后再应用第二个函数产生结果,第二个函数的参数类型得与第一个函数的结果类型匹配,但是第二个函数的返回类型可以使用任何:

val f = thricePlus2 andThen (i => i.toString)
f(3)  // => "11"

  这里f的类型是Int=>String,使用andThen我们能够改变返回结果类型为我们需要的类型,但是输入参数类型必须和初始函数的输出一致。

  Reder Monad是为一元函数定义的Monad,使用andThen作为map操作,一个Reder只是一个Function1,我们能包装这个函数获得map和flatMap方法:

import scalaz.Reader
val triple = Reader((i: Int) => i * 3)
triple(3)   // => 9
val thricePlus2 = triple map (i => i + 2)
thricePlus2(3)  // => 11

  map和flatmap方法让我们使用for解析来定义新的Reader:

val f = for (i <- thricePlus2) yield i.toString
f(3) // => "11"

  如果上面代码比较奇怪,其实它只是下面写法的另外一种表达方式:

val f = thricePlus2 map (i => i.toString)
f(3) // => "11"

 

使用Reader Monad进行依赖注入

  为了使用Reader Monad实现依赖注入,我们只需要使用UserRepository参数定义函数。我们在UserRepository trait中定义一个原始Reader:

trait Users {

  import scalaz.Reader

  def getUser(id: Int) = Reader((userRepository: UserRepository) =>
   userRepository.get(id)
  )

  def findUser(username: String) = Reader((userRepository: UserRepository) =>
    userRepository.find(username)
  )
}

  注意,这些原始返回Reader[UserRepository, User]不是user,它只是一个装饰UserRepository => User,这个装饰可让你使用for解析,它是一个当被输入UserRespository以后会最终返回一个User的函数,实际的依赖注入被延期了。

  我们现在可以在原始Reader上定义所有其他操作了:

object UserInfo extends Users {

  def userEmail(id: Int) = {
    getUser(id) map (_.email)
  }

  def userInfo(username: String) =
    for {
      user <- findUser(username)
      boss <- getUser(user.supervisorId)
    } yield Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
}

  基于原始的Reader使用方法map和flatmap,可以返回一个高阶的Reader,userEmail方法返回的是一个Reader[UserRepository, String],所有与users交互的高阶方法都会直接或间接从原始Reader返回高阶Reader。

  不像在implicits案例,我们不需要在userEmail和userInfo方法中到处使用UserRepository,如果我们需要增加新的依赖,只需要封装它们在一个COnfig对象中,只要改变原始的代码即可。

  比如我们增加一个mail服务:

trait Config {
  def userRepository: UserRepository
  def mailService: MailService
}

  改变原始代码使用Config而不是UserRepository:

trait Users {
  import scalaz.Reader

  def getUser(id: Int) = Reader((config: Config) =>
    config.userRepository.get(id)
  )

  def findUser(username: String) = Reader((config: Config) =>
    config.userRepository.find(username)
  )
}

  我们的UserInfo对象不需要改变,在这个小案例中好像是一个小改变,但是在大型系统中改变巨大,因为在大型系统中有更多高阶Reader。

注射依赖

  那么实际的UserRepositoy在哪里注入呢?所有那些返回Reader方法,在UserRepository以外通过User获得相关数据的时候,实际依赖被延迟到高层了。

  实际中比如我们在Web应用的场景下,这种注射将是在控制器 Action中。

  假设我们有一个UserRepository实现称为UserRepositoryImpl,我们可以定义一个控制器如下:

object Application extends Application(UserRepositoryImpl)

class Application(userRepository: UserRepository) extends Controller with Users {

  def userEmail(id: Int) = Action {
    Ok(run(UserInfo.userEmail(id)))
  }

  def userInfo(username: String) = Action {
    Ok(run(UserInfo.userInfo(username)))
  }

  private def run[A](reader: Reader[UserRepository, A]): JsValue = {
    Json.toJson(reader(userRepository))
  }
}

  object Application使用缺省的实现,我们能使用class Application带有一个模拟的repository测试来初始化一个测试版本,这个案例中,我们也定义了一个便利方法run用来注射UserRepository到一个Reader,然后转换结果到JSON。

 

英文原文

Scala的Cake实现依赖注入是一个谎言

依赖注入

什么是Monad