Scala case 类继承

我有一个基于 Squeryl 的应用程序。我将模型定义为 case 类,主要是因为我发现使用复制方法很方便。

我有两个严格相关的模型。字段是相同的,许多操作是通用的,并且它们存储在同一个 DB 表中。有一些行为只在其中一种情况下有意义,或者在两种情况下都有意义,但是有所不同。

到目前为止,我只使用了一个 case 类,带有一个区分模型类型的标志,所有根据模型类型不同的方法都以一个 if 开头。这很烦人,而且不太安全。

我想要做的是分解祖先案例类中的常见行为和字段,并让两个实际模型从中继承。但是,据我所知,从 case 类继承在 Scala 中是不被允许的,甚至在子类本身是 case 类的情况下也是被禁止的(我的情况不是这样)。

从 case 类继承时应该注意的问题和陷阱是什么?我这样做有意义吗?

114365 次浏览

case classes are perfect for value objects, i.e. objects that don't change any properties and can be compared with equals.

But implementing equals in the presence of inheritance is rather complicated. Consider a two classes:

class Point(x : Int, y : Int)

and

class ColoredPoint( x : Int, y : Int, c : Color) extends Point

So according to the definition the ColorPoint(1,4,red) should be equal to the Point(1,4) they are the same Point after all. So ColorPoint(1,4,blue) should also be equal to Point(1,4), right? But of course ColorPoint(1,4,red) should not equal ColorPoint(1,4,blue), because they have different colors. There you go, one basic property of the equality relation is broken.

update

You can use inheritance from traits solving lots of problems as described in another answer. An even more flexible alternative is often to use type classes. See What are type classes in Scala useful for? or http://www.youtube.com/watch?v=sVMES4RZF-8

My preferred way of avoiding case class inheritance without code duplication is somewhat obvious: create a common (abstract) base class:

abstract class Person {
def name: String
def age: Int
// address and other properties
// methods (ideally only accessors since it is a case class)
}


case class Employer(val name: String, val age: Int, val taxno: Int)
extends Person


case class Employee(val name: String, val age: Int, val salary: Int)
extends Person


If you want to be more fine-grained, group the properties into individual traits:

trait Identifiable { def name: String }
trait Locatable { def address: String }
// trait Ages { def age: Int }


case class Employer(val name: String, val address: String, val taxno: Int)
extends Identifiable
with    Locatable


case class Employee(val name: String, val address: String, val salary: Int)
extends Identifiable
with    Locatable

Since this is an interesting topic to many, let me shed some light here.

You could go with the following approach:

// You can mark it as 'sealed'. Explained later.
sealed trait Person {
def name: String
}


case class Employee(
override val name: String,
salary: Int
) extends Person


case class Tourist(
override val name: String,
bored: Boolean
) extends Person

Yes, you have to duplicate the fields. If you don't, it simply would not be possible to implement correct equality among other problems.

However, you don't need to duplicate methods/functions.

If the duplication of a few properties is that much of an importance to you, then use regular classes, but remember that they don't fit FP well.

Alternatively, you could use composition instead of inheritance:

case class Employee(
person: Person,
salary: Int
)


// In code:
val employee = ...
println(employee.person.name)

Composition is a valid and a sound strategy that you should consider as well.

And in case you wonder what a sealed trait means — it is something that can be extended only in the same file. That is, the two case classes above have to be in the same file. This allows for exhaustive compiler checks:

val x = Employee(name = "Jack", salary = 50000)


x match {
case Employee(name) => println(s"I'm $name!")
}

Gives an error:

warning: match is not exhaustive!
missing combination            Tourist

Which is really useful. Now you won't forget to deal with the other types of Persons (people). This is essentially what the Option class in Scala does.

If that does not matter to you, then you could make it non-sealed and throw the case classes into their own files. And perhaps go with composition.

In these situations I tend to use composition instead of inheritance i.e.

sealed trait IVehicle // tagging trait


case class Vehicle(color: String) extends IVehicle


case class Car(vehicle: Vehicle, doors: Int) extends IVehicle


val vehicle: IVehicle = ...


vehicle match {
case Car(Vehicle(color), doors) => println(s"$color car with $doors doors")
case Vehicle(color) => println(s"$color vehicle")
}

Obviously you can use a more sophisticated hierarchy and matches but hopefully this gives you an idea. The key is to take advantage of the nested extractors that case classes provide