15

有两种方法可以为继承 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

这些做法之间还有哪些其他区别?如果我选择第二种方法,是否有任何错误在等待我?

编辑: 我引用了打开/关闭原则,但有时,我不仅需要根据新的案例类修改函数的输出,还因为代码重构需要修改输入。有比第一个更好的模式吗?如果我想在第一个示例中添加前面提到的功能,这将产生重复输入的丑陋代码:

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

4 回答 4

8

我会选择第一个。

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

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

您的场景在 Java 中有类似的模板/策略模式针对条件

更新:

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

拥有凝聚力的方法比将整个方法混合在一个方法中更好,该方法呈现出方法联合所期望的尽可能多的参数。

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

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

正如这个链接所解释的:

为什么开闭原则很重要:

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

更新 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"}

谢谢类型推断:)

于 2013-05-10T12:22:30.143 回答
2

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

于 2013-05-10T12:25:14.423 回答
2

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

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

语言很难设计出同时具有两种好处的 API - 易于添加类型和添加方法。这个问题称为表达式问题。Scala 提供了 Typeclass 模式来解决这个问题,它允许以特别和选择性的方式向现有类型添加功能。

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

于 2017-01-26T16:14:49.643 回答
1

从 开始Scala 3,您可以使用trait 参数(就像类有参数一样),这在这种情况下大大简化了事情:

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"
于 2019-05-21T20:02:12.713 回答