Scala:如何避免在代码中到处传递相同的对象实例

时间:2019-01-27 17:51:33

标签: scala design-patterns dependency-injection

我有一个复杂的项目,该项目通过对象ConfigAccessor从数据库读取配置,该对象实现了两个基本API:getConfig(name: String)storeConfig(c: Config)

由于当前项目的设计方式,几乎每个组件都需要使用ConfigAccessor与数据库进行对话。因此,作为该组件的对象,很容易将其导入并调用其静态方法。

现在,我正在尝试为该项目构建一些单元测试,在该项目中,配置存储在内存中的hashMap中。因此,首先,我将配置访问器逻辑与其存储区分开(使用蛋糕模式)。这样,我可以在测试时定义自己的ConfigDbComponent

class ConfigAccessor {
   this: ConfigDbComponent => 
   ...

“问题”是,ConfigAccessor现在是一个类,这意味着我必须在应用程序开始时实例化它,并将其传递给任何需要它的人。我想到传递此实例的第一种方法是通过其他组件构造函数。这将变得非常冗长(向项目中的每个构造函数添加一个参数)。

您建议我做什么?有没有办法使用某种设计模式来克服这种冗长性,或者某些外部模拟库更适合于此?

2 个答案:

答案 0 :(得分:1)

是的,“正确”的方法是在构造函数中传递它。您可以通过提供默认参数来减少冗长程度:

class Foo(config: ConfigAccessor = ConfigAccessor) { ... }

围绕它构建了一些“依赖注入”框架,例如guice或spring,但是我不会去那里,因为我不是粉丝。

您还可以继续使用蛋糕图案:

 trait Configuration {
    def config: ConfigAccessor
 }

 trait Foo { self: Configuration => ... }

 class FooProd extends Foo with ProConfig
 class FooTest extends Foo with TestConfig

或者,使用“静态设置器”。它最大程度地减少了对现有代码的更改,但需要可变的状态,而这种状态在scala中实在不受欢迎:

object Config extends ConfigAccessor {
  @volatile private var accessor: ConfigAccessor = _

  def configurate(cfg: ConfigAccessor) = synchronized {
    val old = accessor
    accessor = cfg
    old
  }
  def getConfig(c: String) = Option(accessor).fold(
   throw new IllegalStateException("Not configurated!")
  )(_.getConfig(c))

答案 1 :(得分:-1)

您可以保留全局ConfigAccessor并允许这样的可选访问器:

object ConfigAccessor {
  private lazy val accessor = GetConfigAccessor()

  def getConfig(name: String) = accessor.getConfig(name)
  ...
}

对于生产版本,您可以将逻辑放在GetConfigAccessor中,以基于某些全局配置(例如类型安全配置)选择适当的访问器。

对于单元测试,您可以为不同的测试版本使用不同版本的GetConfigAccessor,这些版本将返回适当的测试实现。

通过设置值lazy,您可以控制初始化的顺序,并在必要时在创建组件之前在初始化代码中进行一些非功能性的可变性操作。


更新以下评论

生产代码将实现GetConfigAccessor,如下所示:

object GetConfigAccessor {
  private val useAws = System.getProperties.getProperty("accessor.aws") == "true"

  def apply(): ConfigAccessor =
    if (useAws) {
      return new AwsConfigAccessor
    } else {
      return new PostgresConfigAccessor
  }
}

AwsConfigAccessorPostgresConfigAccessor都有自己的单元测试,以证明它们符合正确的行为。通过设置适当的系统属性,可以在运行时选择适当的访问器。

对于单元测试,可以使用GetConfigAccessor的更简单实现,例如:

def GetConfigAccessor() = new MockConfigAccessor

单元测试是在一个单元测试框架内完成的,该框架包含许多不属于生产代码的库和模拟对象。它们是单独构建的,不会编译为最终产品。因此,该版本的GetConfigAccessor将成为该单元测试代码的一部分,而不是最终产品的一部分。


说了这么多,我只会将这种模型用于读取静态配置数据,因为这样可以使代码保持功能。 ConfigAccessor只是访问全局常量而不使它们在构造函数中传递的一种便捷方法。

如果您还写入数据,那么它更像是真正的数据库,而不是配置。在这种情况下,我将为每个组件创建自定义访问器,以访问数据库的不同部分。这样,很明显每个组件都可以更新数据的哪些部分。这些访问器将被向下传递到组件,然后可以像往常一样使用适当的模拟实现对它们进行单元测试。

您可能需要将数据分为静态配置动态配置并分别处理。