具有继承和混合的 Scala 可测试代码



我用Java开发了很多代码,并涉足Groovy和Haskell,现在我转向了Scala。

我对Scala

的功能方面感觉相对满意,但我发现自己在Scala的面向对象设计方面有点不稳定,因为它感觉与Java有点不同,特别是由于特征/混合。

我的目标是编写尽可能可测试的代码,在我的 Java 开发中,这些代码始终转化为对

  • 尽可能不变
  • 首选由构造函数注入状态
  • 总是选择组合而不是继承(深受SO帖子的影响,并且可能对这篇文章反应过度(

现在我正试图在这个新的 Scala 领域站稳脚跟,我很难弄清楚我应该在这里采用什么方法,特别是我是否应该出于某些目的开始使用继承

斯卡拉编程(Wampler和Payne;O'Reilly,第2版(有一个考虑因素的部分("好的面向对象设计:题外话"(,我已经阅读了许多关于SO的文章,但我还没有看到明确提到可测试性的设计考虑。本书提供了关于使用继承的建议:

  1. 抽象基类或特征由具体类(包括案例类(子类化一级。
  2. 具体类永远不会被子类化,除了两种情况:
    • 混合在特征中定义的其他行为的类 (...(
    • 仅测试版本,用于促进自动化单元设置。
  3. 当子类化似乎是正确的方法时,请考虑将行为划分为特征并混合这些特征。
  4. 切勿跨父子类型边界拆分逻辑状态。

对SO的一些挖掘也表明,有时混合比组合更可取。

所以本质上我有两个问题:

  1. 在常见情况下,即使考虑到可测试性,使用继承也会更好吗?

  2. 混入是否提供了增强代码可测试性的好方法?

您引用的 Q/A 中的特质用法实际上是在处理混合特质提供的灵活性。

例如,显式扩展特征时,编译器会在编译时锁定类和超类的类型。在此示例中,MyService 是一个LockingFlavorA

trait Locking { // ... }
class LockingFlavorA extends Locking { //... }
class MyService extends LockingFlavorA {
}

使用类型化的自引用时(如指向的问答中所示(:

class MyService {
   this: Locking =>
}

.. Locking可以指Locking本身,也可以指Locking的任何有效子类。然后,作者在调用站点混合了锁定实现,但没有为此目的显式创建新类:

val myService: MyService = new MyService with JDK15Locking

我认为当他们说你可以简化测试时,他们实际上是在谈论使用这个功能来模拟我们Java开发人员通常对组合和模拟对象所做的工作。您只需制作一个模拟Locking实现并在测试期间混合该实现,并为运行时制作一个真正的实现。

对于您的问题:这比使用模拟库和依赖注入更好还是更差?这很难说,但我认为最终很多都会归结为一种技术或另一种技术与代码库的其余部分的配合程度。

如果您已经在使用组合和依赖注入来达到良好的效果,我认为继续使用这种模式可能是个好主意。

如果你刚刚开始,还没有真正需要所有的火炮,或者还没有从哲学上决定依赖注入适合你,你可以从mixins中获得很多里程,而运行时复杂性的成本非常低。

我认为真正的答案将被证明是高度情境化的。

TL;DR 在下面

问题1(我认为这是组合/dep-inj的有用替代方案,但我认为除了简单之外,它没有提供任何重大收益。

问题2(是的,它可以提高可测试性,主要是通过特征实现模拟模拟对象。

所做的可以体验使用混合和组合的组合。

因此,通过示例使用组件将行为混合到特定特征中。下面的示例显示了在一个类中使用多个 dao 层特征的结构。

trait ServiceXXX {
  def findAllByXXX(): Future[SomeClass]
}
trait ServiceYYY {
  def findAllByYYY(): Future[AnotherClass]
}
trait SomeTraitsComponent {
  val serviceXXX: ServiceXXX
  val serviceYYY: ServiceYYY
}
trait SomeTraitsUsingMixing { 
  self: SomeTraitsComponent => 
  def getXXX() = Action.async {
    serviceXXX.findAllByXXX() map { results => 
      Ok(Json.toJson(results))
    }
  }
  def getYYY() = Actiona.async {
    serviceYYY.findAllByYYY() map {results => 
      Ok(Json.toJson(results))
    }
  }
}

之后,您可以声明一个具体组件并通过示例将其绑定到配套对象:

trait ConreteTraitsComponent extends SomeTraitsComponent {
  val serviceXXX = new ConcreteServiceXXX
  val serviceYYY = new ConcreteServiceYYY
}
object SomeTraitsUsingMixing extends ConreteTraitsComponent

使用这种模式 yo 可以轻松创建一个测试组件,并使用 mock 来测试你的 tait/class 的具体行为:

trait SomeTraitsComponentMock {
  val serviceXXX = mock[ServiceXXX]
  val serviceYYY = mock[ServiceYYY]
}
object SomeTraitsUsingMixingMock extends SomeTraitsComponentMock

在你的规范中,你可以使用 ScalaMock 声明控制服务的结果 http://scalamock.org/

最新更新