11

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

我对 Scala 的功能方面感觉比较舒服,但我发现自己在 Scala 中的面向对象设计上有点摇摆不定,因为它感觉与 Java 有点不同,特别是由于特征/混合。

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

  • 尽可能的不变性
  • 更喜欢由构造函数注入状态
  • 总是选择组合而不是继承(受此帖子的严重影响,并且可能对 SO 上的帖子反应过度)

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

Scala 编程(Wampler 和 Payne;O'Reilly,第 2 版)有一个考虑因素的部分(“良好的面向对象设计:题外话”),我已经阅读了一些关于 SO 的帖子,但我还没有看到明确提及可测试性的设计考虑。这本书提供了关于使用继承的建议:

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

对 SO 的一些挖掘还表明,有时 mix-ins 比 composition 更可取

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

  1. 是否存在使用继承更好的常见情况,即使考虑到可测试性?

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

4

2 回答 2

11

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

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

trait Locking { // ... }

class LockingFlavorA extends Locking { //... }

class MyService extends LockingFlavorA {

}

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

class MyService {
   this: Locking =>
}

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

val myService: MyService = new MyService with JDK15Locking

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

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

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

如果您刚刚开始并且还没有真正需要所有这些大炮,或者还没有从哲学上确定依赖注入适合您,那么您可以以非常小的成本从 mixins 获得大量里程在运行时复杂性方面。

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

TL; DR 下面

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

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

于 2015-02-17T19:01:38.340 回答
3

我已经可以体验到使用混合和组合的组合。

因此,例如使用组件将行为混合到特定特征中。下面的示例显示了在一个类中使用多个 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

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

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

object SomeTraitsUsingMixingMock extends SomeTraitsComponentMock

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

于 2015-02-17T06:18:23.723 回答