把方法放在特质或案例类?

时间:2013-05-10 12:18:54

标签: scala object-oriented-analysis

有两种方法可以为Scala中继承相同特征的两个不同类定义方法。

sealed trait Z { def minus: String }
case class A() extends Z { def minus = "a" }
case class B() extends Z { def minus = "b" }

替代方案如下:

sealed trait Z { def minus: String = this match {
    case A() => "a"
    case B() => "b"
}
case class A() extends Z
case class B() extends Z

第一种方法重复方法名称,而第二种方法重复类名。

我认为第一种方法最好用,因为代码是分开的。但是,我发现自己经常使用第二个方法来处理复杂的方法,因此可以非常容易地添加其他参数,例如:

sealed trait Z {
  def minus(word: Boolean = false): String = this match {
    case A() => if(word) "ant" else "a"
    case B() => if(word) "boat" else "b"
}
case class A() extends Z
case class B() extends Z

这些做法之间有什么其他差异?如果我选择第二种方法,是否还有等待我的错误?

修改 我引用了开放/封闭原则,但有时候,我需要修改函数的输出,这取决于新的case类,还需要修改输入,因为代码重构。有没有比第一个更好的模式?如果我想在第一个例子中添加前面提到的功能,这将产生重复输入的丑陋代码:

sealed trait Z { def minus(word: Boolean): String  ; def minus = minus(false) }
case class A() extends Z { def minus(word: Boolean) = if(word) "ant" else "a" }
case class B() extends Z { def minus(word: Boolean) = if(word) "boat" else "b" }

4 个答案:

答案 0 :(得分:7)

我会选择第一个。

为什么?仅保留开放/封闭原则

的确,如果你想添加另一个子类,让我们说case class C,你必须修改supertrait / superclass来插入新条件......丑陋

您的方案在Java中与模板/策略模式类似,而条件

更新:

在上一个场景中,您无法避免输入的“重复”。实际上,Scala中的参数类型是不可推断的。

最好使用内聚方法而不是在一个方法中混合整个方法,这个方法提供了与union期望的方法一样多的参数。

想象一下你的超级方法中的十个条件。如果你无意中改变了每个人的行为怎么办?每次更改都会有风险,每次修改时都应该运行supertrait单元测试......

此外,无意中改变输入参数(不是行为)根本不是“危险的”。为什么?因为编译器会告诉你参数/参数类型不再相关。 如果你想改变它并为每个子类做同样的事情......问你的IDE,它喜欢一键重构这样的东西。

正如link所解释的那样:

为什么开放式原则很重要:

  

无需进行单元测试   无需了解重要且庞大的类的源代码   由于绘图代码被移动到具体的子类,因此在添加新功能时会降低影响旧功能的风险。

更新2:

这里有一个避免输入重复的样本符合您的期望:

sealed trait Z { 
     def minus(word: Boolean): String = if(word) whenWord else whenNotWord
     def whenWord: String
     def whenNotWord: String             
  }

case class A() extends Z { def whenWord = "ant"; def whenNotWord = "a"}

谢谢类型推断:)

答案 1 :(得分:2)

就个人而言,我会远离第二种方法。每次添加Z的新子类时,都必须触摸共享的减号方法,这可能会使与现有实现相关的行为处于危险之中。使用第一种方法添加新子类对现有结构没有潜在的副作用。这里可能有一些开放/封闭原则,你的第二种方法可能会违反它。

答案 2 :(得分:2)

两种方法都可能违反开放/封闭原则。它们彼此正交。第一个允许轻松添加新类型并实现所需的方法,如果您需要将新方法添加到层次结构或重构方法签名到破坏任何客户端代码的点,它会打破打开/关闭原则。毕竟,为什么在Java8接口中添加了默认方法,以便可以扩展旧的API而无需客户端代码进行调整。 这种方法对于OOP来说是典型的。

第二种方法对于FP更为典型。在这种情况下,添加方法很容易,但很难添加新类型(这里打破了O / C)。这是封闭层次结构的好方法,典型的例子是代数数据类型(ADT)。客户无意扩展的标准化协议可能是候选者。

语言很难设计出既有益处又能添加类型以及添加方法的API。此问题称为表达式问题。 Scala提供了Typeclass模式来解决这个问题,它允许以临时和选择的方式向现有类型添加功能。

哪一个更好取决于您的使用案例。

答案 3 :(得分:1)

请注意,使用DottyScala 3的基础),您可以使用trait parameters(就像类具有参数一样),在这种情况下,这大大简化了工作:

trait Z(x: String) { def minus: String = x }
case class A() extends Z("a")
case class B() extends Z("b")
A().minus // "a"
B().minus // "b"