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

Scala testable code with inheritance and mixins

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

我对 Scala 的功能方面感觉比较舒服,但我发现自己在 Scala 中的面向对象设计方面有点不稳定,因为它感觉与 Java 有点不同,特别是由于 traits/mix-ins.

我的目标是编写尽可能可测试的代码,这在我的 Java 开发中一直转化为关注

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

Programming Scala(Wampler 和 Payne;O'Reilly,第 2 版)有一节考虑事项 ("Good Object-Oriented Design: A Digression"),我读了很多关于 SO 的帖子,但我还没有看到明确提到可测试性的设计考虑。该书提供了有关使用继承的建议:

  1. An abstract base class or trait is subclassed one level by concrete classes, including case classes.
  2. Concrete classes are never subclassed, except for two cases:
    • Classes that mix in other behaviors defined in traits (...)
    • Test-only versions to promote automated unit teting.
  3. When subclassing seems like the right approach, consider partitioning behaviors into traits and mix in those traits instead.
  4. Never split logical state across parent-child type boundaries.

一些关于 SO 的挖掘也表明 sometimes mix-ins are preferable to composition

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

  1. 是否存在使用继承会更好的常见情况?

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

我可以体验混合使用和合成的组合。

所以通过示例使用组件将行为混合到特定特征中。下面的示例显示了在 class.

中使用多个 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

使用此模式,您可以轻松创建测试组件并使用模拟来测试 tait/class:

的具体行为
trait SomeTraitsComponentMock {
  val serviceXXX = mock[ServiceXXX]
  val serviceYYY = mock[ServiceYYY]
}

object SomeTraitsUsingMixingMock extends SomeTraitsComponentMock

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

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

例如,当您显式扩展特征时,编译器会在编译时锁定 class 和 super-class 的类型。在此示例中,MyService 是 LockingFlavorA

trait Locking { // ... }

class LockingFlavorA extends Locking { //... }

class MyService extends LockingFlavorA {

}

当您使用类型化的自引用时(如您指向的 Q/A 所示):

class MyService {
   this: Locking =>
}

.. Locking 可以引用 Locking 本身,或 Locking 的任何有效子 class。然后,作者在调用站点混合了锁定实现,而没有为此明确创建新的 class:

val myService: MyService = new MyService with JDK15Locking

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

关于您的问题:这比使用模拟库和依赖项注入好还是坏?很难说,但我认为最终很大程度上取决于一种或另一种技术与您的代码库的其余部分的配合程度。

如果您已经使用组合和依赖注入取得了良好的效果,我认为继续使用该模式可能是个好主意。

如果你刚刚起步并且还没有真正需要所有这些火炮,或者还没有从哲学上决定依赖注入适合你,你可以从 mixin 中获得很多好处以非常小的运行时复杂度为代价。

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

TL;低于 DR

问题 1) 我认为它是 composition/dep-inj 的一种在特定情况下有用的替代方案,但我认为它除了简单之外没有提供任何主要收益。

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