33

如何在 Scala 中创建功能正确的可配置对象?我在Readermonad 上看过 Tony Morris 的视频,但我仍然无法连接这些点。

我有一个硬编码的Client对象列表:

class Client(name : String, age : Int){ /* etc */}

object Client{
  //Horrible!
  val clients  = List(Client("Bob", 20), Client("Cindy", 30))
}

我想Client.clients在运行时确定,可以灵活地从属性文件或数据库中读取它。在 Java 世界中,我会定义一个接口,实现两种类型的源代码,并使用 DI 分配一个类变量:

trait ConfigSource { 
  def clients : List[Client]
}

object ConfigFileSource extends ConfigSource {
  override def clients = buildClientsFromProperties(Properties("clients.properties"))  
  //...etc, read properties files 
}

object DatabaseSource extends ConfigSource { /* etc */ }

object Client {
  @Resource("configuration_source") 
  private var config : ConfigSource = _ //Inject it at runtime  

  val clients = config.clients 
} 

这对我来说似乎是一个非常干净的解决方案(代码不多,意图明确),但这var 确实跳出来了(OTOH,在我看来这并不麻烦,因为我知道它只会注入一次-一次)。

在这种情况下,monad会是什么样Reader子,像我 5 岁一样向我解释,它有什么优势?

4

1 回答 1

47

让我们从你的方法和方法之间的一个简单的、肤浅的区别开始Reader,那就是你不再需要坚持config任何地方。假设您定义了以下模糊巧妙的类型同义词:

type Configured[A] = ConfigSource => A

现在,如果我需要ConfigSource某个函数,比如说一个获取列表中第n个客户端的函数,我可以将该函数声明为“已配置”:

def nthClient(n: Int): Configured[Client] = {
  config => config.clients(n)
}

因此,我们基本上是在无中生有的config情况下,在我们需要的任何时候!闻起来像依赖注入,对吧?现在假设我们想要列表中第一个、第二个和第三个客户的年龄(假设它们存在):

def ages: Configured[(Int, Int, Int)] =
  for {
    a0 <- nthClient(0)
    a1 <- nthClient(1)
    a2 <- nthClient(2)
  } yield (a0.age, a1.age, a2.age)

为此,当然,您需要对map和进行一些适当的定义flatMap。我不会在这里深入讨论,只是简单地说 Scalaz(或Rúnar 的精彩 NEScala 演讲,或您已经看过的 Tony 的演讲)为您提供了所需的一切。

这里重要的一点是ConfigSource依赖及其所谓的注入大多是隐藏的。我们在这里可以看到的唯一“提示”ages是 typeConfigured[(Int, Int, Int)]而不是 simple (Int, Int, Int)。我们不需要在config任何地方显式引用。

顺便说一句,这是我几乎总是喜欢考虑 monad 的方式:它们隐藏了它们的效果,因此它不会污染你的代码流,同时在类型签名中明确声明效果。换句话说,你不必过多地重复自己:你在函数的返回类型中说“嘿,这个函数处理效果 X ”,不要再搞砸了。

在这个例子中,效果当然是从某个固定的环境中读取。您可能熟悉的另一个一元效应包括错误处理:我们可以说它Option隐藏了错误处理逻辑,同时在您的方法类型中明确显示错误的可能性。或者,与阅读相反,Writermonad 隐藏了我们正在写入的内容,同时在类型系统中显式地显示了它的存在。

最后,正如我们通常需要引导一个 DI 框架(在我们通常的控制流之外的某个地方,例如在 XML 文件中),我们也需要引导这个奇怪的 monad。当然,我们的代码会有一些逻辑入口点,例如:

def run: Configured[Unit] = // ...

它最终变得非常简单:因为Configured[A]它只是 function 的类型同义词ConfigSource => A,我们可以将 function 应用于它的“环境”:

run(ConfigFileSource)
// or
run(DatabaseSource)

达达!因此,与传统的 Java 风格的 DI 方法相比,这里没有任何“魔法”发生。可以说,唯一的魔法封装在我们类型的定义中,Configured以及它作为 monad 的行为方式。最重要的是,类型系统让我们诚实地知道发生在哪个“领域”依赖注入:任何有类型的东西Configured[...]都在 DI 世界中,而没有它的东西则不是。我们根本无法在老式 DI 中实现这一点,在这种情况下,一切都可能由魔法管理,因此您并不真正知道代码的哪些部分可以安全地在 DI 框架之外重用(例如,在您的单元内)测试,或完全在其他项目中)。


更新:我写了一篇Reader文,更详细地解释了。

于 2012-06-29T03:41:15.460 回答