web-dev-qa-db-ja.com

Scalaケースクラスの継承

Squerylに基づいたアプリケーションがあります。私はモデルをケースクラスとして定義します。ほとんどの場合、コピーメソッドが便利だと感じているからです。

厳密に関連する2つのモデルがあります。フィールドは同じで、多くの操作が共通しており、同じDBテーブルに保存されます。 ただし 2つのケースのいずれかでのみ意味をなす動作、または両方のケースで意味を持ちながら異なる動作があります。

これまで、モデルのタイプを区別するフラグを持つ単一のケースクラスのみを使用し、モデルのタイプに基づいて異なるすべてのメソッドはifで始まります。これは迷惑であり、タイプセーフではありません。

私がしたいことは、祖先のケースクラスの一般的な動作とフィールドを考慮し、2つの実際のモデルがそれを継承することです。しかし、私が理解している限りでは、Scalaではケースクラスからの継承は嫌われており、サブクラス自体がケースクラス(私のケースではない)である場合でも禁止されています。

ケースクラスから継承する際に注意する必要がある問題と落とし穴は何ですか?私の場合、そうすることは理にかなっていますか?

83
Andrea

コードの重複なしにケースクラスの継承を回避する私の好ましい方法は、いくぶん明白です。共通の(抽象)基本クラスを作成します。

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


さらにきめ細かくする場合は、プロパティを個々の特性にグループ化します。

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
103

これは多くの人にとって興味深いトピックなので、ここでいくつかの光を当てましょう。

次のアプローチを使用できます。

// 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

はい、フィールドを複製する必要があります。そうしないと、単純に正しい平等を実装することができません 他の問題の中で

ただし、メソッド/関数を複製する必要はありません。

いくつかのプロパティを複製することが非常に重要な場合は、通常のクラスを使用しますが、FPにうまく適合しないことに注意してください。

または、継承の代わりに構成を使用することもできます。

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

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

作曲は有効で健全な戦略であり、同様に考慮する必要があります。

また、封印された特性が何を意味するのか疑問に思う場合は、同じファイル内でのみ拡張できるものです。つまり、上記の2つのケースクラスは同じファイル内にある必要があります。これにより、徹底的なコンパイラーチェックが可能になります。

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

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

エラーを与えます:

warning: match is not exhaustive!
missing combination            Tourist

これは本当に便利です。これで、他のタイプのPersons(人)を扱うことを忘れないでください。これは、基本的にScalaのOptionクラスが行うことです。

それが問題にならない場合は、封印せずにケースクラスを独自のファイルにスローできます。そして、おそらく作曲に行きます。

40
Kai Sellgren

ケースクラスは、値オブジェクト、つまりプロパティを変更せず、等しいと比較できるオブジェクトに最適です。

しかし、継承の存在下でequalsを実装するのはかなり複雑です。 2つのクラスを検討してください。

class Point(x : Int, y : Int)

そして

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

したがって、定義によれば、ColorPoint(1,4、red)はPoint(1,4)と等しくなければなりません。 ColorPoint(1,4、blue)もPoint(1,4)に等しいはずですよね?ただし、当然のことながら、ColorPoint(1,4、red)はColorPoint(1,4、blue)と同じではありません。色が異なるためです。そこに行くと、等式関係の1つの基本的なプロパティが壊れています。

update

別の回答で説明されているように、多くの問題を解決する特性からの継承を使用できます。さらに柔軟な代替手段は、多くの場合、型クラスを使用することです。 Scalaの型クラスとは? または http://www.youtube.com/watch?v=sVMES4RZF-8 を参照してください。

13
Jens Schauder

これらの状況では、継承の代わりに構成を使用する傾向があります。

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")
}

明らかに、より洗練された階層と一致を使用できますが、うまくいけばアイデアが得られます。重要なのは、ケースクラスが提供するネストされたエクストラクターを利用することです

6
user404345