web-dev-qa-db-fra.com

Scala héritage de classe de cas

J'ai une application basée sur Squeryl. Je définis mes modèles en tant que classes de cas, car je trouve pratique d’avoir des méthodes de copie.

J'ai deux modèles qui sont strictement liés. Les champs sont les mêmes, de nombreuses opérations sont en commun et doivent être stockées dans la même table de base de données. Mais il existe un comportement qui n'a de sens que dans l'un des deux cas, ou qui a du sens dans les deux cas mais qui est différent.

Jusqu'à présent, je n'utilisais qu'une seule classe de cas, avec un drapeau qui distingue le type du modèle, et toutes les méthodes qui diffèrent en fonction du type du modèle commencent par un if. C'est ennuyeux et pas tout à fait sûr.

Ce que je voudrais faire est de factoriser le comportement commun et les champs dans une classe de cas ancêtre et de faire en sorte que les deux modèles actuels en héritent. Mais, pour autant que je sache, hériter de classes de cas est mal vu dans Scala, et même interdit si la sous-classe est elle-même une classe de cas (pas mon cas).

Quels sont les problèmes et les pièges que je devrais connaître en héritant d'une classe de cas? Est-ce logique dans mon cas de le faire?

83
Andrea

Ma manière préférée d'éviter l'héritage de classe de cas sans duplication de code est assez évidente: créez une classe de base commune (abstraite):

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


Si vous voulez être plus fin, regroupez les propriétés en traits individuels:

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
Malte Schwerhoff

Puisqu'il s'agit d'un sujet intéressant pour beaucoup, laissez-moi vous éclairer ici.

Vous pourriez aller avec l'approche suivante:

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

Oui, vous devez dupliquer les champs. Sinon, il ne serait tout simplement pas possible de mettre en œuvre une égalité correcte parmi d'autres problèmes .

Cependant, vous n'avez pas besoin de dupliquer les méthodes/fonctions.

Si la duplication de quelques propriétés revêt une telle importance pour vous, utilisez des classes régulières, mais souvenez-vous qu'elles ne correspondent pas bien FP bien.

Vous pouvez également utiliser la composition au lieu de l'héritage:

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

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

La composition est une stratégie valable et valable que vous devriez également envisager.

Et au cas où vous vous demanderiez ce que signifie un trait scellé - c'est quelque chose qui ne peut être étendu que dans le même fichier. C'est-à-dire que les deux classes de cas ci-dessus doivent être dans le même fichier. Cela permet des vérifications exhaustives du compilateur:

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

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

Donne une erreur:

warning: match is not exhaustive!
missing combination            Tourist

Ce qui est vraiment utile. Maintenant, vous n'oublierez pas de traiter avec les autres types de Persons (personnes). C’est essentiellement ce que la classe Option de Scala) fait.

Si cela ne vous dérange pas, vous pouvez le rendre non scellé et jeter les classes de cas dans leurs propres fichiers. Et peut-être aller avec la composition.

40
Kai Sellgren

les classes de cas sont idéales pour les objets de valeur, c'est-à-dire les objets qui ne changent aucune propriété et peuvent être comparés à égaux.

Mais la mise en œuvre d'égal à égal en présence d'héritage est plutôt compliquée. Considérons deux classes:

class Point(x : Int, y : Int)

et

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

Ainsi, selon la définition, ColorPoint (1,4, rouge) doit être égal au point (1,4) car il s’agit du même point. Donc, ColorPoint (1,4, bleu) devrait également être égal à Point (1,4), non? Mais bien sûr, ColorPoint (1,4, rouge) ne doit pas être égal à ColorPoint (1,4, bleu), car ils ont des couleurs différentes. Voilà, une propriété de base de la relation d’égalité est brisée.

mettre à jour

Vous pouvez utiliser l'héritage des traits pour résoudre de nombreux problèmes, comme décrit dans une autre réponse. Une alternative encore plus flexible consiste souvent à utiliser des classes de types. Voir Quelles sont les classes de type dans Scala utile pour?? ou http://www.youtube.com/watch?v=sVMES4RZF-8 =

13
Jens Schauder

Dans ces situations, j’ai tendance à utiliser la composition au lieu de l’héritage, c’est-à-dire.

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

De toute évidence, vous pouvez utiliser une hiérarchie et des correspondances plus sophistiquées, mais j'espère que cela vous donne une idée. La clé consiste à tirer parti des extracteurs imbriqués fournis par les classes de cas.

6
user404345