用继承和混合编写可测试的Scala代码

11

我用Java开发了很多代码,并尝试使用Groovy和Haskell, 这让我接触到了Scala。

我在Scala的函数式方面感觉比较熟练,但是对象导向设计上感觉有些不稳定,因为它与Java有所不同,特别是由于traits/mix-ins的存在。

我的目标是编写尽可能易于测试的代码,在我的Java开发中,这通常转化为关注:

现在我正在努力适应这个新的Scala领域,但我很难弄清楚我应该采用什么方法,特别是是否应该开始为某些目的使用继承。

Programming Scala(Wampler and Payne; O'Reilly, 2nd Edition)中有一个考虑因素的部分(“良好的面向对象设计:一次偏离”),我已经阅读了一些SO的帖子,但我没有看到显式提到测试性的设计考虑。该书提供了有关使用继承的建议:

  1. 一个抽象的基类或trait被具体类(包括case classes)子类化一级。
  2. 除两种情况外,不会对具体类进行子类化:
    • 混合其他在traits中定义的行为的类(...)
    • 仅用于促进自动化单元测试的测试版本。
  3. 当子类化似乎是正确的方法时,请考虑将行为划分为traits并混入这些traits。
  4. 永远不要将逻辑状态分裂在父-子类型边界上。

一些SO的探讨也表明,有时mix-ins优于组合

因此,本质上我有两个问题:

  1. 有常见情况需要使用继承吗?即使考虑到可测试性,是使用继承更好的选择吗?

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

2个回答

11

您提到的问答中使用的特质,实际上是处理通过混入特质提供的灵活性。

例如,当您显式扩展一个特质时,编译器会在编译时锁定类和超类的类型。 在此示例中,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 实现并将其混合,而在运行时则制作一个真正的实现。
对于你的问题:这比使用模拟库和依赖注入更好还是更差?很难说,但我认为最终很多问题都将与一种技术或另一种技术如何与您的代码库的其余部分配合有关。
如果您已经有效地使用组合和依赖注入,那么继续使用该模式可能是一个好主意。
如果您刚开始,并且实际上没有需要使用所有这些工具,或者还没有从哲学上决定使用依赖注入,您可以通过在运行时复杂性上付出很小的代价来获得一些效益。
TL; DR:这是一种情境上有用的替代方法,但除了简单性外,我认为它并没有提供任何重大收益。是的,它可以通过特质实现来模拟模拟对象,从而改善可测试性。

谢谢。我非常感激这个答案,因为它对我的参考资料提供了很好的评论,并直接回答了问题。 - Erik Madsen
1
没问题,Scala是一门有趣的语言,了解一些更加深奥的语言特性如何与人们已经在世界上做的事情相结合是很好的。这是一个好问题。 - Rich Henry
嗯...从它那个零分的成绩来看,我想它本来可以更好。但是我得到了答案,这才是最重要的部分。 - Erik Madsen
1
哈哈,人们总是自私的,包括我在内。我加了分,确实认为这是一个好问题。 - Rich Henry

3

我曾通过混合和组合使用技术来获得良好的经验。

所以,例如使用组件将行为混入特定特质。下面的示例展示了在类中使用多个数据访问层特质的结构。

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/)来控制服务的结果。


谢谢您的回答。您能详细解释一下为什么选择将ConcreteTraitsComponent作为特质而不是类吗? - Erik Madsen
我更喜欢尽可能将函数单元声明为特质,因为在Scala中只能从多个特质中扩展,而不是从一个类中扩展。 - toggm

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