Scala 用于依赖项注入的读卡器Monad:多个依赖项、嵌套调用

Scala 用于依赖项注入的读卡器Monad:多个依赖项、嵌套调用,scala,dependency-injection,scalaz,Scala,Dependency Injection,Scalaz,当被问及Scala中的依赖注入时,相当多的答案指向使用阅读器Monad,或者是Scalaz中的Monad,或者是您自己的Monad。有许多非常清晰的文章描述了该方法的基本原理(例如),但我没有找到一个更完整的示例,并且我没有看到该方法相对于更传统的“手动”DI的优势(参见)。很可能我遗漏了一些重要的观点,因此提出了这个问题 作为一个示例,让我们假设我们有以下类: trait Datastore { def runQuery(query: String): List[String] } trait

当被问及Scala中的依赖注入时,相当多的答案指向使用阅读器Monad,或者是Scalaz中的Monad,或者是您自己的Monad。有许多非常清晰的文章描述了该方法的基本原理(例如),但我没有找到一个更完整的示例,并且我没有看到该方法相对于更传统的“手动”DI的优势(参见)。很可能我遗漏了一些重要的观点,因此提出了这个问题

作为一个示例,让我们假设我们有以下类:

trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }

class FindUsers(datastore: Datastore) {
  def inactive(): Unit = ()
}

class UserReminder(findUser: FindUsers, emailServer: EmailServer) {
  def emailInactive(): Unit = ()
}

class CustomerRelations(userReminder: UserReminder) {
  def retainUsers(): Unit = {}
}
在这里,我使用类和构造函数参数对事物进行建模,这与“传统”DI方法很好地结合在一起,但是这种设计有两个好的方面:

  • 每个功能都有明确列举的依赖项。我们假设依赖关系是功能正常工作所必需的
  • 这些依赖项隐藏在各种功能中,例如,
    userremement
    不知道
    FindUsers
    需要数据存储。这些功能甚至可以在单独的编译单元中实现
  • 我们只使用纯Scala;这些实现可以利用不可变类、高阶函数,“业务逻辑”方法可以返回封装在
    IO
    monad中的值,如果我们想要捕获效果等
如何使用阅读器monad对其进行建模?最好保留上述特征,以便清楚每个功能需要什么样的依赖关系,并隐藏一个功能对另一个功能的依赖关系。请注意,使用
class
es更像是一个实现细节;也许使用Reader monad的“正确”解决方案会使用其他方法

我确实找到了一个建议:

  • 使用具有所有依赖项的单个环境对象
  • 使用本地环境
  • “冻糕”图案
  • 类型索引映射
然而,除了对于这样一件简单的事情来说有点过于复杂(但这是主观的),在所有这些解决方案中,例如
retainUsers
方法(调用
emailInactive
,调用
inactive
以查找非活动用户)需要了解
数据存储的依赖性,能够正确调用嵌套函数-还是我错了


对于这样的“业务应用程序”,在哪些方面使用Reader Monad会比仅使用构造函数参数更好

我认为主要区别在于,在您的示例中,当对象被实例化时,您正在注入所有依赖项。阅读器monad基本上构建了一个越来越复杂的函数来调用给定的依赖项,然后返回到最高层。在这种情况下,注入发生在函数最终被调用时

一个直接的优势是灵活性,特别是如果您可以一次性构造monad,然后希望将其用于不同的注入依赖项。正如你所说,一个缺点是可能不够清晰。在这两种情况下,中间层只需要知道它们的直接依赖关系,因此它们都像DI宣传的那样工作。

如何对这个示例建模 如何使用阅读器monad对其进行建模

我不确定这是否应该与读者一起建模,但可以通过:

  • 将类编码为函数,从而使代码更易于读取器使用
  • 以便于理解和使用的方式与读者一起编写函数
  • 就在开始之前,我需要告诉您一些小的示例代码调整,我觉得这些调整对这个答案是有益的。 第一个变化是关于
    FindUsers.inactive
    方法。我让它返回
    List[String]
    以便可以使用地址列表 在
    用户提醒.emailInactive
    方法中。我还为方法添加了简单的实现。最后,示例将使用 以下是阅读器monad的手卷版本:

    case class Reader[Conf, T](read: Conf => T) { self =>
    
      def map[U](convert: T => U): Reader[Conf, U] =
        Reader(self.read andThen convert)
    
      def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
        Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))
    
      def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
        Reader[BiggerConf, T](extractFrom andThen self.read)
    }
    
    object Reader {
      def pure[C, A](a: A): Reader[C, A] =
        Reader(_ => a)
    
      implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
        Reader(read)
    }
    
    建模步骤1。将类编码为函数 也许这是可选的,我不确定,但后来它使理解的效果更好。 注意,结果函数是curry。它还将以前的构造函数参数作为其第一个参数(参数列表)。 那样

    变成

    object Foo {
      def bar: Dep => Arg => Res = ???
    }
    // usage: val result = Foo.bar(dependency)(arg)
    
    请记住,
    Dep
    Arg
    Res
    类型都可以是完全任意的:元组、函数或简单类型

    以下是初始调整后转换为函数的示例代码:

    trait Datastore { def runQuery(query: String): List[String] }
    trait EmailServer { def sendEmail(to: String, content: String): Unit }
    
    object FindUsers {
      def inactive: Datastore => () => List[String] =
        dataStore => () => dataStore.runQuery("select inactive")
    }
    
    object UserReminder {
      def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
        emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
    }
    
    object CustomerRelations {
      def retainUsers(emailInactive: () => Unit): () => Unit =
        () => {
          println("emailing inactive users")
          emailInactive()
        }
    }
    
    这里需要注意的一点是,特定的函数不依赖于整个对象,而只依赖于直接使用的部分。 其中,在OOP版本中,
    userrementer.emailInactive()
    实例将调用
    userFinder.inactive()
    这里它只调用
    inactive()
    -在第一个参数中传递给它的函数

    请注意,本规范展示了问题中的三个理想属性:

  • 很清楚每个功能需要什么样的依赖关系
  • 隐藏一个功能对另一个功能的依赖关系
  • retainUsers
    方法不需要知道数据存储依赖关系
  • 建模步骤2。使用读取器编写函数并运行它们 Reader monad只允许编写所有依赖于同一类型的函数。情况往往并非如此。在我们的例子中
    FindUsers.inactive
    取决于
    数据存储
    用户提醒。emailInactive
    取决于
    EmailServer
    。为了解决这个问题 可以引入一个包含所有依赖项的新类型(通常称为Config),然后进行更改 这些函数都依赖于它,并且只从中获取相关数据。 从依赖关系管理的角度来看,这显然是错误的,因为这样可以使这些函数也相互依赖 一开始他们不应该知道的类型

    福图
    trait Datastore { def runQuery(query: String): List[String] }
    trait EmailServer { def sendEmail(to: String, content: String): Unit }
    
    object FindUsers {
      def inactive: Datastore => () => List[String] =
        dataStore => () => dataStore.runQuery("select inactive")
    }
    
    object UserReminder {
      def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
        emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
    }
    
    object CustomerRelations {
      def retainUsers(emailInactive: () => Unit): () => Unit =
        () => {
          println("emailing inactive users")
          emailInactive()
        }
    }
    
    object Main extends App {
    
      case class Config(dataStore: Datastore, emailServer: EmailServer)
    
      val config = Config(
        new Datastore { def runQuery(query: String) = List("john.doe@fizzbuzz.com") },
        new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
      )
    
      import Reader._
    
      val reader = for {
        getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
        emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
        retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
      } yield retainUsers
    
      reader.read(config)()
    
    }
    
    getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
    
      case class DataStore()
      case class EmailServer()
    
      def f1(db:DataStore):List[String] = List("john@test.com", "james@test.com", "maria@test.com")
    
      def f2_raw(emailServer: EmailServer, usersToEmail:List[String]):Unit =
    
        usersToEmail.foreach(user => println(s"emailing ${user} using server ${emailServer}"))
    
      import cats.data.Reader
    
      val f2 = (server:EmailServer) => (usersToEmail:List[String]) => f2_raw(server, usersToEmail)
    
      case class CombinedConfig(dataStore:DataStore, emailServer: EmailServer)
    
      val r1 = Reader(f1)
      val r2 = Reader(f2)
    
      val r1g = r1.local((c:CombinedConfig) => c.dataStore)
      val r2g = r2.local((c:CombinedConfig) => c.emailServer)
    
      val composition = for {
        u <- r1g
        e <- r2g
      } yield e(u)
    
      val myConfig = CombinedConfig(DataStore(), EmailServer())
    
      println("Invoking Composition")
      composition.run(myConfig)