从技术角度来看,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 class
es 是为贫血方法设计的)。因此,在消除方法之后 - 一个只能与你的 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{}
我在 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 class
es 是为贫血方法设计的)。因此,在消除方法之后 - 一个只能与你的 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{}