gpt4 book ai didi

scala - 用于依赖注入(inject) : multiple dependencies, 嵌套调用的 Reader Monad

转载 作者:行者123 更新时间:2023-12-03 05:07:26 25 4
gpt4 key购买 nike

当被问及 Scala 中的依赖注入(inject)时,很多答案都指向使用 Reader Monad,要么来自 Scalaz,要么只是滚动你自己的。有许多非常清晰的文章描述了该方法的基础知识(例如 Runar's talkJason's blog ),但我没有找到更完整的示例,并且我没有看到这种方法的优势,例如更传统的“手动”DI(请参阅 the guide I wrote )。很可能我错过了一些重要的点,因此问题。

举个例子,让我们假设我们有这些类:

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 方法非常配合,但是这种设计有几个优点:
  • 每个功能都有明确列举的依赖项。我们有点假设依赖项是功能正常工作所必需的
  • 依赖项在功能之间隐藏,例如UserReminder 不知道 FindUsers 需要数据存储。功能甚至可以在单独的编译单元中
  • 我们只使用纯 Scala;实现可以利用不可变类、高阶函数,如果我们想捕获效果等,“业务逻辑”方法可以返回包装在 IO monad 中的值。

  • 这如何用 Reader monad 建模?保留上面的特征会很好,这样就可以清楚每个功能需要什么样的依赖关系,并隐藏一个功能对另一个功能的依赖关系。请注意,使用 class es 更多的是实现细节;也许使用 Reader monad 的“正确”解决方案会使用其他东西。

    我确实找到了一个 somewhat related question,它表明:
  • 使用具有所有依赖项的单个环境对象
  • 使用本地环境
  • “冻糕”图案
  • 类型索引映射

  • 然而,除了(但这是主观的)对于这么简单的事情来说有点太复杂了,在所有这些解决方案中,例如 retainUsers 方法(调用 emailInactive ,调用 inactive 来查找非事件用户)需要了解 Datastore 依赖项,才能正确调用嵌套函数 - 还是我错了?

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

    最佳答案

    如何建模这个例子

    How could this be modelled with the Reader monad?



    我不确定这是否应该用 Reader 建模,但它可以通过:
  • 将类编码为函数,这使得代码与 Reader
  • 一起玩得更好
  • 将函数与 Reader 组合在一起用于理解并使用它

  • 就在开始之前,我需要告诉您我认为对这个答案有益的小示例代码调整。
    第一个变化是关于 FindUsers.inactive 方法。我让它返回 List[String] 以便可以使用地址列表
    UserReminder.emailInactive 方法中。我还为方法添加了简单的实现。最后,示例将使用一个
    以下是 Reader 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. 将类编码为函数

    也许这是可选的,我不确定,但后来它使理解看起来更好。
    请注意,生成的函数是柯里化的。它还将以前的构造函数参数作为它们的第一个参数(参数列表)。
    那样
    class Foo(dep: Dep) {
    def bar(arg: Arg): Res = ???
    }
    // usage: val result = new Foo(dependency).bar(arg)

    变成
    object Foo {
    def bar: Dep => Arg => Res = ???
    }
    // usage: val result = Foo.bar(dependency)(arg)

    请记住, DepArgRes 类型中的每一个都可以是完全任意的:元组、函数或简单类型。

    这是初始调整后的示例代码,转换为函数:
    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 版本 UserReminder.emailInactive() 实例中,哪里会调用 userFinder.inactive() 在这里它只调用 inactive()- 在第一个参数中传递给它的函数。

    请注意,该代码展示了问题中的三个理想属性:
  • 很清楚每个功能需要什么样的依赖
  • 隐藏一个功能对另一个功能的依赖关系
  • retainUsers 方法不需要知道数据存储依赖项

  • 建模步骤 2. 使用 Reader 组合函数并运行它们

    Reader monad 允许你只组合依赖相同类型的函数。这通常不是个案。在我们的例子中 FindUsers.inactive 取决于 DatastoreUserReminder.emailInactive 上的 EmailServer 。为了解决这个问题
    可以引入一种包含所有依赖项的新类型(通常称为 Config),然后更改
    函数,因此它们都依赖于它,并且只从中获取相关数据。
    从依赖管理的角度来看,这显然是错误的,因为这样您就可以使这些功能也依赖
    关于他们一开始不应该知道的类型。

    幸运的是,有一种方法可以使函数与 Config 一起工作,即使它只接受它的一部分作为参数。
    这是一个名为 local 的方法,在 Reader 中定义。它需要提供一种从 Config 中提取相关部分的方法。

    应用于手头示例的这些知识如下所示:
    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)()

    }

    使用构造函数参数的优势

    In what aspects would using the Reader Monad for such a "business application" be better than just using constructor parameters?



    我希望通过准备这个答案,我可以更容易地自己判断它在哪些方面击败了普通的构造函数。
    然而,如果我要列举这些,这是我的 list 。免责声明:我有 OOP 背景,我可能不喜欢 Reader 和 Kleisli
    完全,因为我不使用它们。
  • 一致性 - 无论理解有多短/多长,它只是一个阅读器,您可以轻松地与另一个阅读器组合
    例如,也许只引入了另一种 Config 类型并在其上添加了一些 local 调用。这一点是 IMO
    而是品味问题,因为当您使用构造函数时,没有人会阻止您编写任何您喜欢的东西,
    除非有人做了一些愚蠢的事情,比如在构造函数中工作,这在 OOP 中被认为是一种不好的做法。
  • Reader 是一个 monad,因此它可以获得与此相关的所有好处 - sequencetraverse 方法免费实现。
  • 在某些情况下,您可能会发现最好只构建一次阅读器并将其用于广泛的配置。
    使用构造函数,没有人会阻止您这样做,您只需要为每个 Config 重新构建整个对象图
    传入。虽然我对此没有任何问题(我什至更喜欢在每次申请时都这样做),但事实并非如此
    对于许多人来说,这是一个显而易见的想法,原因我只能推测。
  • Reader 插入您更多地使用功能,这将更好地与主要以 FP 风格编写的应用程序一起使用。
  • 阅读器分离关注点;您可以在不提供依赖项的情况下创建、与所有内容交互、定义逻辑。实际供应后,分开。 (在这一点上感谢 Ken Scrambler)。这是 Reader 经常听到的优点,但对于普通构造函数也可以做到。

  • 我还想告诉我在 Reader 中我不喜欢的地方。
  • 营销。有时我得到的印象是,Reader 是针对所有类型的依赖项进行营销的,如果是这样,则没有区别
    session cookie 或数据库。对我来说,将 Reader 用于几乎不变的对象(例如电子邮件)几乎没有意义
    本示例中的服务器或存储库。对于这样的依赖,我找到了普通的构造函数和/或部分应用的函数
    好多了。本质上 Reader 为您提供了灵活性,因此您可以在每次调用时指定您的依赖项,但是如果您
    真的不需要那个,你只需要交税。
  • 隐式沉重 - 使用没有隐式的 Reader 会使示例难以阅读。另一方面,当你隐藏
    使用隐式的嘈杂部分并产生一些错误,编译器有时会给你难以破译的消息。
  • 仪式与 purelocal 并为此创建自己的配置类/使用元组。 Reader 强制你添加一些代码
    这与问题域无关,因此在代码中引入了一些噪音。另一方面,申请
    使用构造函数的通常使用工厂模式,这也是来自问题域之外的,所以这个弱点不是
    严肃的。

  • 如果我不想将我的类转换为带有函数的对象怎么办?

    你要。从技术上讲, 可以 避免这种情况,但是看看如果我不将 FindUsers 类转换为对象会发生什么。 for comprehension 的相应行如下所示:
    getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)

    这不是那么可读,是吗?关键是 Reader 对函数进行操作,所以如果你还没有它们,你需要内联构造它们,这通常不是那么漂亮。

    关于scala - 用于依赖注入(inject) : multiple dependencies, 嵌套调用的 Reader Monad,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29174500/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com