Scala 中创建功能正常的可配置对象?我已经看过托尼·莫里斯(Tony Morris)关于Reader
monad的视频,但我仍然无法将这些点联系起来。
我有一个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,在我看来这并不麻烦,因为我知道它会被注入一次并且只有一次)。
在这种情况下,Reader
monad 会是什么样子,像我 5 岁一样向我解释一下,它有什么优势?
让我们从你的方法和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
属于Configured[(Int, Int, Int)]
类型,而不仅仅是(Int, Int, Int)
。我们不需要在任何地方显式引用config
。
顺便说一句,这是我几乎总是喜欢思考monads的方式:它们隐藏了它们的效果,因此它不会污染代码的流,同时在类型签名中显式声明效果。 换句话说,你不需要重复太多:你在函数的返回类型中说"嘿,这个函数处理效果X",不要再弄乱它了。
在这个例子中,效果当然是从某个固定环境中读取。 您可能熟悉的另一个一元效应包括错误处理:我们可以说
Option
隐藏了错误处理逻辑,同时在方法类型中明确了错误的可能性。 或者,与阅读相反,Writer
monad 隐藏了我们正在写的东西,同时在类型系统中明确存在它。
现在最后,就像我们通常需要引导一个 DI 框架(在我们通常的控制流之外的某个地方,例如在 XML 文件中)一样,我们也需要引导这个奇怪的 monad。当然,我们的代码会有一些逻辑入口点,例如:
def run: Configured[Unit] = // ...
它最终非常简单:由于Configured[A]
只是函数ConfigSource => A
的类型同义词,我们可以将函数应用于其"环境":
run(ConfigFileSource)
// or
run(DatabaseSource)
哒!因此,与传统的Java风格的DI方法相比,我们在这里没有任何"魔力"。可以说,唯一的魔力被封装在我们的Configured
类型的定义以及它作为monad的行为方式中。最重要的是,类型系统让我们诚实地了解发生在哪个"领域"依赖注入中:任何具有 Configured[...]
类型的内容都在 DI 世界中,而任何没有它的东西都不是。我们根本不会在老式的 DI 中得到这一点,在老式 DI 中,一切都可能由魔法管理,因此您并不真正知道代码的哪些部分可以在 DI 框架之外安全地重用(例如,在您的单元测试中,或者完全在其他项目中)。
更新:我写了一篇博客文章,更详细地解释了Reader
。