Vererbung von Scala-Fallklassen

86

Ich habe eine Anwendung, die auf Squeryl basiert. Ich definiere meine Modelle als Fallklassen, vor allem, weil ich Kopiermethoden für zweckmäßig halte.

Ich habe zwei Modelle, die eng miteinander verbunden sind. Die Felder sind gleich, viele Operationen sind gemeinsam und sie sollen in derselben DB-Tabelle gespeichert werden. Es gibt jedoch ein Verhalten, das nur in einem der beiden Fälle Sinn macht oder in beiden Fällen Sinn macht, aber unterschiedlich ist.

Bisher habe ich nur eine einzige Fallklasse verwendet, mit einem Flag, das den Typ des Modells unterscheidet, und alle Methoden, die sich je nach Typ des Modells unterscheiden, beginnen mit einem if. Das ist nervig und nicht ganz typsicher.

Was ich tun möchte, ist, das allgemeine Verhalten und die Felder in einer Ahnenfallklasse zu berücksichtigen und die beiden tatsächlichen Modelle davon erben zu lassen. Soweit ich weiß, ist das Erben von Fallklassen in Scala verpönt und sogar verboten, wenn die Unterklasse selbst eine Fallklasse ist (nicht mein Fall).

Was sind die Probleme und Fallstricke, die ich beim Erben von einer Fallklasse beachten sollte? Ist es in meinem Fall sinnvoll, dies zu tun?

Andrea
quelle
1
Könnten Sie nicht von einer Nicht-Fall-Klasse erben oder ein gemeinsames Merkmal erweitern?
Eduardo
Ich bin nicht sicher. Die Felder werden im Vorfahren definiert. Ich möchte Kopiermethoden, Gleichheit usw. basierend auf diesen Feldern erhalten. Wenn ich das Elternteil als abstrakte Klasse und die Kinder als Fallklasse deklariere, werden dann die auf dem Elternteil definierten Parameter berücksichtigt?
Andrea
Ich denke nicht, Sie müssen Requisiten sowohl in der abstrakten Eltern- (oder Eigenschaft-) als auch in der Zielfallklasse definieren. Am Ende ist viel
los

Antworten:

114

Meine bevorzugte Methode zur Vermeidung der Vererbung von Fallklassen ohne Codeduplizierung liegt auf der Hand: Erstellen Sie eine gemeinsame (abstrakte) Basisklasse:

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


Wenn Sie feinkörniger sein möchten, gruppieren Sie die Eigenschaften in einzelne Merkmale:

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
Malte Schwerhoff
quelle
82
Wo ist das "ohne Codeduplizierung", von dem Sie sprechen? Ja, ein Vertrag wird zwischen der
Fallklasse
5
@virtualeyes Richtig, Sie müssen die Eigenschaften noch wiederholen. Sie müssen jedoch keine Methoden wiederholen, die normalerweise mehr Code als die Eigenschaften enthalten.
Malte Schwerhoff
1
Ja, ich hatte nur gehofft, die Duplizierung von Eigenschaften zu umgehen - eine andere Antwort weist auf Typklassen als mögliche Problemumgehung hin. Ich bin mir nicht sicher, wie ich jedoch eher darauf ausgerichtet bin, Verhaltensweisen wie Merkmale zu mischen, aber flexibler. Nur Boilerplate re: case Klassen, kann damit leben, wäre ziemlich unglaublich, wenn es anders wäre, könnte wirklich große Schwaden von Eigenschaftsdefinitionen hacken
virtualeyes
1
@virtualeyes Ich stimme voll und ganz zu, dass es großartig wäre, wenn die Wiederholung von Eigenschaften auf einfache Weise vermieden werden könnte. Ein Compiler-Plugin könnte sicherlich den Trick machen, aber ich würde das nicht einfach nennen.
Malte Schwerhoff
12
@virtualeyes Ich denke, dass es bei der Vermeidung von Codeduplizierungen nicht nur darum geht, weniger zu schreiben. Für mich geht es mehr darum, nicht denselben Code in verschiedenen Teilen Ihrer Anwendung zu haben, ohne dass eine Verbindung zwischen ihnen besteht. Mit dieser Lösung sind alle Unterklassen an einen Vertrag gebunden. Wenn sich also die übergeordnete Klasse ändert, kann die IDE Ihnen helfen, die Teile des Codes zu identifizieren, die Sie reparieren müssen.
Daniel
40

Da dies für viele ein interessantes Thema ist, möchte ich hier etwas Licht ins Dunkel bringen.

Sie könnten mit dem folgenden Ansatz gehen:

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

Ja, Sie müssen die Felder duplizieren. Wenn Sie nicht tun, wäre es einfach nicht möglich, korrekte Gleichheit zu implementieren unter anderem Probleme .

Sie müssen jedoch keine Methoden / Funktionen duplizieren.

Wenn die Duplizierung einiger Eigenschaften für Sie so wichtig ist, verwenden Sie reguläre Klassen, aber denken Sie daran, dass sie nicht gut zu FP passen.

Alternativ können Sie Komposition anstelle von Vererbung verwenden:

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

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

Komposition ist eine gültige und solide Strategie, die Sie ebenfalls berücksichtigen sollten.

Und falls Sie sich fragen, was ein versiegeltes Merkmal bedeutet - es kann nur in derselben Datei erweitert werden. Das heißt, die beiden oben genannten Fallklassen müssen sich in derselben Datei befinden. Dies ermöglicht umfassende Compilerprüfungen:

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

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

Gibt einen Fehler:

warning: match is not exhaustive!
missing combination            Tourist

Welches ist wirklich nützlich. Jetzt werden Sie nicht vergessen, sich mit den anderen Arten von Persons (Menschen) zu befassen . Dies ist im Wesentlichen das, was die OptionKlasse in Scala tut.

Wenn Ihnen das nichts ausmacht, können Sie es nicht versiegeln und die Fallklassen in ihre eigenen Dateien werfen. Und vielleicht mit Komposition gehen.

Kai Sellgren
quelle
1
Ich denke, dass das def namein der Eigenschaft sein muss val name. Mein Compiler gab mir mit dem ersteren nicht erreichbare Code-Warnungen.
BAR
13

case-Klassen eignen sich perfekt für Wertobjekte, dh Objekte, die keine Eigenschaften ändern und mit Gleichheitswerten verglichen werden können.

Die Implementierung von Gleichheit bei vorhandener Vererbung ist jedoch ziemlich kompliziert. Betrachten Sie zwei Klassen:

class Point(x : Int, y : Int)

und

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

Entsprechend der Definition sollte der Farbpunkt (1,4, rot) gleich dem Punkt (1,4) sein, sie sind schließlich der gleiche Punkt. ColorPoint (1,4, blau) sollte also auch gleich Punkt (1,4) sein, oder? Aber natürlich sollte ColorPoint (1,4, rot) nicht gleich ColorPoint (1,4, blau) sein, da sie unterschiedliche Farben haben. Los geht's, eine grundlegende Eigenschaft der Gleichheitsrelation ist gebrochen.

aktualisieren

Sie können die Vererbung von Merkmalen verwenden, um viele Probleme zu lösen, wie in einer anderen Antwort beschrieben. Eine noch flexiblere Alternative ist häufig die Verwendung von Typklassen. Siehe Wofür sind Typklassen in Scala nützlich?oder http://www.youtube.com/watch?v=sVMES4RZF-8

Jens Schauder
quelle
Ich verstehe und stimme dem zu. Was sollten Sie also tun, wenn Sie eine Bewerbung haben, die sich beispielsweise mit Arbeitgebern und Arbeitnehmern befasst? Angenommen, sie teilen alle Felder (Name, Adresse usw.), wobei der einzige Unterschied in einigen Methoden besteht - zum Beispiel möchte man vielleicht definieren, Employer.fire(e: Emplooyee)aber nicht umgekehrt. Ich würde gerne zwei verschiedene Klassen machen, da sie tatsächlich verschiedene Objekte darstellen, aber ich mag auch nicht die Wiederholung, die entsteht.
Andrea
Haben Sie hier ein Beispiel für einen Typklassenansatz mit der Frage? dh in Bezug auf
Fallklassen
@virtualeyes Man könnte völlig unabhängige Typen für die verschiedenen Arten von Entitäten haben und Typklassen bereitstellen, um das Verhalten bereitzustellen. Diese Typklassen könnten Vererbung genauso nützlich wie nützlich verwenden, da sie nicht an den semantischen Vertrag von Fallklassen gebunden sind. Wäre es in dieser Frage nützlich? Weiß nicht, die Frage ist nicht spezifisch genug, um sie zu erzählen.
Jens Schauder
@JensSchauder Es scheint, dass Merkmale in Bezug auf das Verhalten dasselbe bieten, nur weniger flexibel als Typklassen. Ich komme zu der Nicht-Duplizierung von Eigenschaften von Fallklassen, etwas, das Merkmale oder abstrakte Klassen normalerweise vermeiden würden.
Virtualeyes
7

In diesen Situationen tendiere ich dazu, Komposition anstelle von Vererbung zu verwenden, dh

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

Natürlich können Sie eine komplexere Hierarchie und Übereinstimmungen verwenden, aber hoffentlich erhalten Sie eine Idee. Der Schlüssel besteht darin, die verschachtelten Extraktoren zu nutzen, die Fallklassen bereitstellen


quelle
3
Dies scheint die einzige Antwort hier zu sein, die wirklich keine doppelten Felder hat
Alan Thomas