从技术角度来看,mix-in traits 可以扩展 case 类 吗?

May mix-in traits extend case classes from a technical perspective?

我在 SO 上反复读到 case classes 不应扩展,因为 case class 默认实现相等方法,这会导致相等问题。但是,如果一个特征扩展了一个案例class,那也是有问题的吗?

case class MyCaseClass(string: String)
trait MyTrait extends MyCaseClass
val myCT = new MyCaseClass("hi") with MyTrait

我想这可以归结为一个问题,MyTrait 是否只能被迫混合到 MyCaseClass 的实例化中,或者 MyTrait 是否继承了 MyTrait 的 class 成员(字段值和方法)并因此覆盖他们。在第一种情况下,可以从 MyCaseClass 继承,在后一种情况下就不行了。但它是哪一个?

为了调查,我用

推进了我的实验
trait MyTrait extends MyCaseClass {
  def equals(m: MyCaseClass): Boolean = false
  def equals(m: MyCaseClass with MyTrait): Boolean = false
}
val myC = new MyCaseClass("hi")
myCT.equals(myC) // res0: Boolean = true

让我相信使用了 MyCaseClass 的 equals,而不是 MyTrait 的 equals。这表明特征可以扩展案例 class(而 class 扩展案例 class 则不行)。

但是,我不确定我的实验是否合法。你能解释一下这件事吗?

基本上,trait 可以扩展 any class,因此最好将它们与常规 classes(OOP 样式)一起使用。

无论如何,不​​管你的技巧如何,你的平等合同仍然被打破(请注意标准 Java 的 equals 是在 Any 上定义的,默认情况下使用让我们说HashMap 甚至 ==):

scala> trait MyTrait extends MyCaseClass {
     |   override def equals(m: Any): Boolean = false
     | }
defined trait MyTrait

scala> val myCT = new MyCaseClass("hi") with MyTrait
myCT: MyCaseClass with MyTrait = MyCaseClass(hi)

scala> val myC = new MyCaseClass("hi")
myC: MyCaseClass = MyCaseClass(hi)

scala> myC.equals(myCT)
res4: Boolean = true

scala> myCT.equals(myC)
res5: Boolean = false

此外,Hashcode/equals 不是唯一的原因...

用另一个 class 扩展 case class 是不自然的,因为 case class 代表 ADT,所以它只建模 数据 - 不是行为.

这就是为什么您不应该向它添加任何方法(在 OOD 术语中,case classes 是为贫血方法设计的)。因此,在消除方法之后 - 一个只能与你的 class 混合的特征变得毫无意义,因为将特征与 case classes 一起使用是为了模拟分离(所以特征在这里是接口 - 而不是混合 - ins):

//your data model (Haskell-like):
data Color = Red | Blue

//Scala
trait Color
case object Red extends Color
case object Blue extends Color

如果 Color 只能与 Blue 混合 - 它与

相同
data Color = Blue

即使您需要更复杂的数据,例如

//your data model (Haskell-like):
data Color = BlueLike | RedLike
data BlueLike = Blue | LightBlue
data RedLike = Red | Pink

//Scala
trait Color  extends Red
trait BlueLike extends Color
trait RedLike extends Color
case class Red(name: String) extends RedLike //is OK
case class Blue(name: String) extends BlueLike //won't compile!!

Color 绑定为仅 Red 似乎不是一个好方法(通常),因为您将无法 case object Blue extends BlueLike

P.S。 Case classes 不打算用于 OOP 风格(mix-in 是 OOP 的一部分)——它们与 type-classes/pattern-matching 交互得更好。因此,我建议将复杂的类方法逻辑从 case class 中移开。一种方法可能是:

trait MyCaseClassLogic1 {
  def applyLogic(cc: MyCaseClass, param: String) = {}
}

trait MyCaseClassLogic2 extends MyCaseClassLogic {
  def applyLogic2(cc: MyCaseClass, param: String) = {}
}

object MyCaseClassLogic extends MyCaseClassLogic1 with MyCaseClassLogic2

您可以在此处使用自类型或 trait extends,但您很容易注意到它是多余的,因为 applyLogic 仅绑定到 MyCaseClass :)

另一种方法是 implicit class(或者您可以尝试更高级的东西,例如 type-classes)

implicit class MyCaseClassLogic(o: MyCaseClass) {
  def applyLogic = {}
}

P.S.2 贫血 vs 富人。 ADT 不完全是贫血模型,因为它适用于不可变(无状态)数据。如果您阅读 the article,Martin Fowler 的方法是 OOP/OOD,默认情况下它是有状态的 - 这就是他在文章的大部分部分中所假设的,暗示服务层和业务层应该具有独立的状态。在 FP 中(至少在我的实践中)我们仍然将领域逻辑与服务逻辑分开,但我们也将操作与数据分开(在每一层中),这是另一回事。

扩展大小写 classes 是一种不好的做法(通常),因为它具有具体含义——"data container" (POJO / ADT)。例如,Kotlin 不允许这样做。

此外,如果你真的想要一些特征来扩展 case class,你最好使用 requires 依赖(以避免 case classes 继承的陷阱):

scala> case class A()
defined class A

scala> trait B { self: A => }
defined trait B

scala> new B{}
<console>:15: error: illegal inheritance;
self-type B does not conform to B's selftype B with A
   new B{}