在Scala中使用蛋糕模式和函数之间的区别 - 为什么蛋糕模式有用?
在Scala中使用蛋糕模式和函数之间的区别 - 为什么蛋糕模式有用?
我在思考在Scala中使用函数和Cake模式进行依赖注入的区别。我得出了以下理解,想知道这个理解是否正确。
让我们想象一个依赖图。
1) 如果我们使用函数作为构建块,那么图中的节点将是函数,边将是参数。
2) 如果我们使用特质作为构建块(就像Cake模式中那样),那么图中的节点将是特质,边将是抽象成员。
那么Cake模式的目的是什么?为什么2比1更好?它是为了粗粒度的组织。图1可以通过将函数分组到特质中来简化,这样我们就得到了一个更小、更易理解的图2。相关概念的分组/聚类是一种压缩形式,并创建了理解(我们需要记住的东西更少,就能理解)。
这里是另一个比较(Cake模式与包系统之间的比较):
Cake类似于将相关函数分组到包中,但它超越了这一点,因为使用命名空间(包/对象)会导致依赖关系被硬编码,而Cake则用特质和自类型注释/抽象成员替代了包/对象和导入(import)。包和Cake模式之间的区别在于,在使用Cake时,一个依赖的实际实现可以改变,而在使用包时是无法改变的。
我不知道这些类比是否有意义,如果不对,请纠正我,如果对,请向我保证。我仍然在努力理解Cake模式以及如何与我已经理解的概念(函数、包)相关联。
在Scala中,依赖注入(DI)通常使用获取器/设置器(即函数)和/或构造函数参数来实现。获取器/设置器的方法可能如下所示:
trait Logger { // fancy logging stuff } class NeedsALogger { private var l: Logger = _ def logger: Logger = l def logger_=(newLogger: Logger) { l = newLogger } // uses a Logger here }
但是我不太喜欢使用获取器/设置器的方法。它无法保证依赖项是否被注入。如果使用某些DI框架,可以强制要求注入某些内容,但这样一来,DI就不再与框架无关了。如果使用构造函数的方法,那么无论使用哪个框架,都必须在实例化时提供依赖项:
class NeedsALogger(logger: Logger) {
// uses a Logger here
}
那么,Cake Pattern又是如何适应这个问题的呢?首先,让我们将我们的示例改写为Cake Pattern的形式:
class NeedsALogger { logger: Logger => // Uses a Logger here }
让我们来讨论一下`logger: Logger =>`。这是一个self-type,它简单地将`Logger`的成员引入作用域,而无需扩展`Logger`。`NeedsALogger`不是一个`Logger`,所以我们不想扩展它。然而,`NeedsALogger`需要一个`Logger`,这就是我们通过self-type来实现的。我们要求在创建`NeedsALogger`时必须提供一个`Logger`。使用方法如下:
trait FooLogger extends Logger { // full implementation of Logger } trait BarLogger extends Logger { // full implementation of Logger } val a = new NeedsALogger with FooLogger val b = new NeedsALogger with BarLogger val c = new NeedsALogger // compile-time error!
正如你所看到的,我们可以使用任何一种方法来实现相同的效果。对于大多数DI来说,构造函数的方法就足够了,所以你可以根据自己的喜好来选择。个人而言,我喜欢self-type和Cake Pattern,但我也看到很多人避免使用它。
要继续了解Cake Pattern的具体内容,请查看这里。这是一个很好的下一步,如果你想了解更多信息的话。