Scalaケースクラスの継承


88

Squerylをベースにしたアプリケーションがあります。モデルをケースクラスとして定義します。これは主に、コピーメソッドがあると便利だからです。

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

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

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

ケースクラスから継承する際に知っておくべき問題と落とし穴は何ですか?私の場合、そうすることは理にかなっていますか?


1
ケース以外のクラスから継承したり、共通の特性を拡張したりできませんでしたか?
エドゥアルド

私はわかりません。フィールドは祖先で定義されています。それらのフィールドに基づいて、コピー方法や平等などを取得したいと思います。親を抽象クラスとして宣言し、子をケースクラスとして宣言した場合、親で定義されたパラメーターが考慮されますか?
アンドレア

そうではないと思いますが、抽象親(またはトレイト)とターゲットケースクラスの両方で小道具を定義する必要があります。結局、ロットはボイラープレートですが、少なくともタイプセーフです
virtualeyes 2012年

回答:


118

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

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

83
あなたが話しているこの「コードの重複なし」はどこにありますか?はい、契約はケースクラスとその親の間で定義されていますが、まだ小道具X2を入力しています
virtualeyes 2012年

5
@virtualeyes本当です、あなたはまだプロパティを繰り返す必要があります。ただし、メソッドを繰り返す必要はありません。通常、メソッドはプロパティよりも多くのコードになります。
マルタシュヴェルホフ2012年

1
ええ、プロパティの重複を回避することを望んでいました-別の答えは、可能な回避策として型クラスを示唆しています。しかし、どのように特性のような行動の混合に向けられているように見えるかはわかりませんが、より柔軟です。ちょうど定型的なre:ケースクラスは、それと一緒に暮らすことができ、そうでなければかなり信じられないでしょう、プロパティ定義の素晴らしい帯を本当にハックすることができます
virtualeyes 2012年

1
@virtualeyesプロパティの繰り返しを簡単に回避できれば素晴らしいと思います。コンパイラプラグインは確かにそのトリックを行うことができますが、私はそれを簡単な方法とは呼びません。
マルタシュヴェルホフ2012年

13
@virtualeyesコードの重複を避けることは、書くことを減らすことだけではないと思います。私にとっては、アプリケーションのさまざまな部分に同じコードを配置せずに、それらを接続しないことです。このソリューションでは、すべてのサブクラスがコントラクトに関連付けられているため、親クラスが変更された場合、IDEは修正が必要なコードの部分を特定するのに役立ちます。
ダニエル

40

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

次のアプローチをとることができます。

// 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(人)に対処することを忘れないでください。これは本質的OptionにScalaのクラスが行うことです。

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


1
def name特性の中にある必要があると思いますval name。私のコンパイラは、前者では到達不能コードの警告を出していました。
BAR

13

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

しかし、継承が存在する場合の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)と等しくなければなりません。結局、それらは同じPointです。つまり、ColorPoint(1,4、blue)もPoint(1,4)と等しくなければなりませんよね?ただし、もちろん、ColorPoint(1,4、red)は色が異なるため、ColorPoint(1,4、blue)と同じであってはなりません。さあ、同値関係の基本的な性質の1つが壊れています。

更新

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


私はそれを理解し、同意します。したがって、たとえば雇用主や従業員を扱うアプリケーションがある場合は、何を提案する必要がありますか。それらがすべてのフィールド(名前、住所など)を共有していると仮定します。唯一の違いは、いくつかのメソッドにあります。たとえば、定義したいEmployer.fire(e: Emplooyee)がその逆はしたくない場合があります。実際には異なるオブジェクトを表すため、2つの異なるクラスを作成したいと思いますが、発生する繰り返しも嫌いです。
アンドレア

ここに質問がある型クラスアプローチの例がありますか?つまり、ケースクラスに関して
virtualeyes 2012年

@virtualeyesさまざまな種類のエンティティに対して完全に独立した型を持ち、動作を提供するために型クラスを提供することができます。これらの型クラスは、ケースクラスのセマンティックコントラクトに拘束されないため、継承をできるだけ便利に使用できます。この質問に役立ちますか?わからない、質問は伝えるのに十分具体的ではありません。
Jens Schauder 2012年

@JensSchauder特性は、動作に関して同じことを提供し、型クラスよりも柔軟性が低いように見えます。ケースクラスのプロパティが重複しないようになっています。これは、特性または抽象クラスが通常回避するのに役立つものです。
virtualeyes 2012年

7

これらの状況では、私は継承の代わりにコンポジションを使用する傾向があります。

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

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


3
これは、本当に重複するフィールドがないここでの唯一の答えのようです
Alan Thomas
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.