如何在不硬编码的情况下使用蛋糕模式进行依赖注入?

75

我刚读了 Cake 模式的文章,并且很喜欢它。但是,在我看来,使用依赖注入的一个关键原因是您可以通过 XML 文件或命令行参数来改变正在使用的组件。

那么,在 Cake 模式中如何处理 DI 的这个方面呢?我看到的所有示例都涉及静态地混合特质。

5个回答

56

由于Scala中的混入特质是静态完成的,如果您想要对混入到对象中的特质进行变化,可以基于某些条件创建不同的对象。

让我们以典型的“蛋糕模式”为例。您的模块被定义为特质,而应用程序则构建为一个简单的对象,其中混合了许多功能。

val application =
    new Object
extends Communications
   with Parsing
   with Persistence
   with Logging
   with ProductionDataSource
application.startup
现在,所有这些模块都有很好的自我类型声明来定义它们之间的模块间依赖关系,所以只有在您的所有模块间依赖项存在、唯一且类型正确时,该行才会被编译。特别地,Persistence模块具有一个自我类型,它表示实现Persistence必须同时实现DataSource,这是一个抽象的模块trait。由于ProductionDataSource继承自DataSource,一切都很棒,应用程序构造行 编译通过。
但是,如果你想使用不同的DataSource,指向某个本地数据库进行测试,该怎么办呢?进一步假设您不能仅从某个属性文件中加载不同的配置参数并重用ProductionDataSource。在这种情况下,您可以定义一个新的trait TestDataSource,它扩展了DataSource,并将其混合到其中。您甚至可以根据命令行标志动态执行此操作。
val application = if (test)
  new Object
    extends Communications
      with Parsing
      with Persistence
      with Logging
      with TestDataSource
else
  new Object
    extends Communications
      with Parsing
      with Persistence
      with Logging
      with ProductionDataSource

application.startup

现在看起来有些冗长,特别是如果您的应用程序需要在多个方面上进行变化构建。但好的一面是,通常您在应用程序中只有一个这样的条件构建逻辑块(或者最坏情况下每个可识别的组件生命周期只有一次),因此至少痛苦是最小化的并且与您的其他逻辑分隔开来。


纯规范的蛋糕模式,尽管我认为OP正在寻找一种将trait添加到已实例化对象的方法。问题在于它只能在构建时完成。 - Kevin Wright
10
@Dave:你真的认为你的“if”语句已经可以在生产环境中使用,并且是你会部署到企业软件中的东西吗?在我看来,这是非常糟糕的代码,因为它未能将部署问题(哪个数据库)与代码问题(应用程序如何组合)分开。查找数据库应该在JNDI树中完成;它绝不能被硬编码,因为要进行更改需要重新部署。 - Ant Kutschera
6
除非没有其他选择,否则在生产代码中我不会以那种方式做。这只是我所知道的对于原始问题最好的答案,原始问题确实暗示需要在运行时更改应用程序的构成方式。仅仅说“你永远不需要这样做,因为一切都可以通过单独配置组件来完成”是回避问题。 - Dave Griffith
1
如果您非常关注蛋糕模式的软件工程方面,那么“测试”语句可以简单地嵌入到您的单元测试中,而else或生产条款则会留在主应用程序中。 - jhclark
2
@jhclark 生产、阶段、开发、测试:当您可能需要处理多个数据库,具体取决于运行时条件时,如何处理这些情况?上面提到的JNDI树引用是一个很好的选择,但我不确定它是否涵盖了所有基础知识,而蛋糕实现可以覆盖所有内容,只需更多的样板文件,正如Dave在他的答案中提到的,“痛苦”;-) - virtualeyes
在你的例子中,我看到你正在做 val application = ... - 你有能力使用这些依赖项定义你的应用程序。 然而,假设你的主要应用程序是一个Java类,其中包含Java代码使用的Scala库。在这种情况下,如何使用“生产”类呢? - Kevin Meredith

29

Scala也是一种脚本语言。因此您的配置XML可以是一个Scala脚本。它是类型安全的,不是另一种语言。

只需查看启动即可:

scala -cp first.jar:second.jar startupScript.scala

并不是非常不同于:

java -cp first.jar:second.jar com.example.MyMainClass context.xml

您始终可以使用 DI,但您还有另一个工具。


5
简单来说,Scala目前没有内置支持动态mixin的功能。
我正在开发autoproxy-plugin来支持此功能,但目前已经暂停,因为在2.9版本发布后,编译器将具备新特性,使得这项任务更加容易实现。
与此同时,达到几乎完全相同的功能的最佳方法是将您动态添加的行为实现为包装类,然后添加一个隐式转换回包装成员。

这些新功能有解释说明吗? - pedrofurla
@pedrofurla - 在源代码中 :) 2.9编译器在类型检查阶段单元失败后有更好的回滚方式,这主要是为演示编译器(如eclipse和ensime)使用而实现的。对我来说很重要,因为autoproxy-plugin使用了一种两次输入的打字技术,一次用于生成委托方法所需的类型信息,另一次用于在合成委托之前无法输入的单元。在第一次失败的过程中,在符号表中留下了不一致性的问题。 - Kevin Wright
1
@pedrofurla - 并不是说这些都与Scala编程有关,只适用于那些使用编译器插件进行某种特定技巧的人。 - Kevin Wright

3

在 AutoProxy 插件可用之前,实现该效果的一种方法是使用委托:

trait Module {
  def foo: Int
}

trait DelegatedModule extends Module {
  var delegate: Module = _
  def foo = delegate.foo
}

class Impl extends Module {
  def foo = 1
}

// later
val composed: Module with ... with ... = new DelegatedModule with ... with ...
composed.delegate = choose() // choose is linear in the number of `Module` implementations

但是要注意的是,这种方法更冗长,如果在trait内部使用var,则必须小心初始化顺序。另一个缺点是,如果在Module上存在路径相关类型,则无法轻松使用委托。

但是,如果有大量不同的实现可以变化,那么与列出所有可能组合的情况相比,它可能会为您节省代码。


新的DelegatedModule?您可以声明“new”特征吗?在这种特定的上下文中,下划线是什么意思?我很困惑。 - Ryan Leach

0

嗯,我通常喜欢Lift作为一个框架和库。但在这种情况下,它并不是一个真正好的答案,据我所见。主要原因是程序的正确性实际上并没有被编译器检查。该库期望任何DI调用都可能失败,即使每个DI调用的结果都是“可选”的。 - VasiliNovikov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接