ada*_*amw 85 dependency-injection scala scalaz
当被问及Scala中的依赖注入时,很多答案都指向使用Reader Monad,无论是来自Scalaz还是只是自己编写.有很多非常明确的文章描述了这种方法的基础知识(例如,Runar的演讲,Jason的博客),但我没有设法找到更完整的例子,我没有看到这种方法的优势超过例如更多传统的"手动"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 = {}
}
Run Code Online (Sandbox Code Playgroud)
在这里,我使用类和构造函数参数进行建模,这与"传统"DI方法非常相似,但是这个设计有几个好的方面:
UserReminder不知道FindUsers需要数据存储.功能甚至可以在单独的编译单元中IO如果我们想要捕获效果等,"业务逻辑"方法可以返回包含在monad中的值.怎么能用Reader monad建模呢?保留上面的特性会很好,因此很清楚每个功能需要什么样的依赖关系,并隐藏一个功能与另一个功能的依赖关系.请注意,使用classes更多的是实现细节; 也许使用Reader monad的"正确"解决方案会使用其他东西.
我找到了一个有点相关的问题,暗示:
然而,除了(但这是主观的)有点过于复杂以至于这样一个简单的事情,在所有这些解决方案中,例如retainUsers方法(调用emailInactive,调用inactive以查找非活动用户)将需要知道Datastore依赖关系,能够正确调用嵌套函数 - 或者我错了?
在哪些方面使用Reader Monad来实现这样的"业务应用程序"要比使用构造函数参数更好?
Prz*_*wka 36
怎么能用Reader monad建模呢?
我不确定这是否应该用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)
}
Run Code Online (Sandbox Code Playgroud)
也许这是可选的,我不确定,但后来它使得理解看起来更好.注意,结果函数是curry.它还将前构造函数参数作为其第一个参数(参数列表).那样
class Foo(dep: Dep) {
def bar(arg: Arg): Res = ???
}
// usage: val result = new Foo(dependency).bar(arg)
Run Code Online (Sandbox Code Playgroud)
变
object Foo {
def bar: Dep => Arg => Res = ???
}
// usage: val result = Foo.bar(dependency)(arg)
Run Code Online (Sandbox Code Playgroud)
请记住,每一个的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()
}
}
Run Code Online (Sandbox Code Playgroud)
这里需要注意的一点是,特定功能不依赖于整个对象,而只依赖于直接使用的部分.OOP版本UserReminder.emailInactive()实例userFinder.inactive()在此处调用它只是调用inactive()
- 在第一个参数中传递给它的函数.
请注意,代码展示了问题中的三个理想属性:
retainUsers 方法不应该知道数据存储依赖性Reader monad允许您只编写所有依赖于相同类型的函数.通常情况并非如此.在我们的例子
FindUsers.inactive依赖于Datastore与UserReminder.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)()
}
Run Code Online (Sandbox Code Playgroud)
在哪些方面使用Reader Monad来实现这样的"业务应用程序"要比使用构造函数参数更好?
我希望通过准备这个答案,我更容易判断自己在哪些方面会胜过普通的构造函数.然而,如果我要列举这些,这是我的清单.免责声明:我有OOP背景,我可能不会完全欣赏Reader和Kleisli,因为我不使用它们.
local在它上面喷洒一些调用.这一点是IMO而不是品味问题,因为当你使用构造函数时,没有人会阻止你构建你喜欢的任何东西,除非有人做了一些愚蠢的事情,比如在构造函数中做工作,这被认为是OOP中的一个不好的做法.sequence,traverse自由实现的方法.我还想在Reader中说出我不喜欢的内容.
pure,local并创建自己的配置类/使用元组.Reader会强制您添加一些与问题域无关的代码,因此会在代码中引入一些噪音.另一方面,使用构造函数的应用程序通常使用工厂模式,这也来自问题域之外,因此这种弱点并不严重.你要.你在技术上能够避免这种情况,但只是看看,如果我不转换会发生什么FindUsers类的对象.理解的各个方面如下:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
Run Code Online (Sandbox Code Playgroud)
哪个不那么可读,是吗?关键是Reader对函数进行操作,所以如果你还没有它们,你需要内联构造它们,这通常不是那么漂亮.