Héritage de classe de cas Scala

89

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

J'ai deux modèles strictement liés. Les champs sont identiques, de nombreuses opérations sont communes et doivent être stockées dans la même table DB. Mais il y a 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'ai utilisé qu'une seule classe de cas, avec un indicateur qui distingue le type de modèle, et toutes les méthodes qui diffèrent en fonction du type de 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 et les champs communs dans une classe de cas ancêtre et que les deux modèles réels en héritent. Mais, pour autant que je sache, hériter des classes de cas est mal vu dans Scala, et est 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 dont je dois être conscient en héritant d'une classe de cas? Est-ce que cela a du sens dans mon cas de le faire?

Andrea
la source
1
Ne pourriez-vous pas hériter d'une classe sans cas, ou étendre un trait commun?
Eduardo
Je ne suis pas sûr. Les champs sont définis dans l'ancêtre. Je veux obtenir des méthodes de copie, l'égalité, etc. en fonction de ces champs. Si je déclare le parent en tant que classe abstraite et les enfants en tant que classe de cas, prendra-t-il en compte les paramètres définis sur le parent?
Andrea
Je ne pense pas, vous devez définir les accessoires à la fois dans le parent abstrait (ou trait) et dans la classe de cas cible. En fin de compte, beaucoup de choses passe-partout, mais tapez au moins sûr
virtualeyes

Réponses:

119

Ma façon préférée d'éviter l'héritage de classe de cas sans duplication de code est quelque peu évidente: créer 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 précis, 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
Malte Schwerhoff
la source
83
où est ce "sans duplication de code" dont vous parlez? Oui, un contrat est défini entre la classe de cas et ses parents, mais vous êtes toujours en train de taper les accessoires X2
virtualeyes
5
@virtualeyes C'est vrai, vous devez toujours répéter les propriétés. Vous n'êtes pas obligé de répéter les méthodes, qui représentent généralement plus de code que les propriétés.
Malte Schwerhoff
1
ouais, j'espérais juste contourner la duplication des propriétés - une autre réponse fait allusion aux classes de types comme solution de contournement possible; Je ne sais pas comment, cependant, semble plus orienté vers le mélange de comportements, comme les traits, mais plus flexible. Juste passe-partout re: classes de cas, peut vivre avec, serait assez incroyable s'il en était autrement, pourrait vraiment pirater de grandes parties de définitions de propriété
Virtualeyes
1
@virtualeyes Je suis tout à fait d'accord pour dire que ce serait formidable si la répétition des propriétés pouvait être évitée facilement. Un plugin de compilateur pourrait sûrement faire l'affaire, mais je n'appellerais pas cela un moyen facile.
Malte Schwerhoff
13
@virtualeyes Je pense qu'éviter la duplication de code ne consiste pas seulement à écrire moins. Pour moi, il s'agit plus de ne pas avoir le même morceau de code dans différentes parties de votre application sans aucun lien entre elles. Avec cette solution, toutes les sous-classes sont liées à un contrat, donc si la classe parent change, l'EDI sera en mesure de vous aider à identifier les parties du code que vous devez corriger.
Daniel
40

Puisqu'il s'agit d'un sujet intéressant pour beaucoup, permettez-moi de faire la lumière ici.

Vous pouvez opter pour 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 d'implémenter 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 est si importante pour vous, utilisez des classes régulières, mais rappelez-vous qu'elles ne correspondent pas bien à FP.

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 solide que vous devriez également considérer.

Et au cas où vous vous demandez ce que signifie un trait scellé, c'est quelque chose qui ne peut être étendu que dans le même fichier. Autrement dit, 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 vous occuper des autres types de Persons (personnes). C'est essentiellement ce que fait la Optionclasse dans Scala.

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

Kai Sellgren
la source
1
Je pense que le def namedans le trait doit être val name. Mon compilateur me donnait des avertissements de code inaccessibles avec l'ancien.
BAR
13

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

Mais la mise en œuvre d'égaux en présence d'héritage est assez compliquée. Considérez deux classes:

class Point(x : Int, y : Int)

et

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

Donc, selon la définition, le ColorPoint (1,4, rouge) doit être égal au point (1,4), ils sont le même point après tout. 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é fondamentale de la relation d'égalité est rompue.

mise à 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 À quoi servent les classes de types dans Scala? ou http://www.youtube.com/watch?v=sVMES4RZF-8

Jens Schauder
la source
Je comprends et suis d'accord avec cela. Alors, que suggérez-vous de faire lorsque vous avez une application qui traite, par exemple, des employeurs et des employés. Supposons qu'ils partagent tous les champs (nom, adresse, etc.), la seule différence étant dans certaines méthodes - par exemple, on peut vouloir définir Employer.fire(e: Emplooyee)mais pas l'inverse. Je voudrais créer deux classes différentes, car elles représentent en fait des objets différents, mais je n'aime pas non plus la répétition qui en résulte.
Andrea
obtenu un exemple d'approche de classe de type avec la question ici? ie en ce qui concerne les classes de cas
virtualeyes
@virtualeyes On pourrait avoir des types complètement indépendants pour les différents types d'entités et fournir des classes de types pour fournir le comportement. Ces classes de types pourraient utiliser l'héritage autant qu'utile, car elles ne sont pas liées par le contrat sémantique des classes de cas. Serait-ce utile dans cette question? Je ne sais pas, la question n'est pas assez précise pour être expliquée.
Jens Schauder
@JensSchauder il semblerait que les traits fournissent la même chose en termes de comportement, juste moins flexibles que les classes de types; J'arrive à la non-duplication des propriétés de classe de cas, quelque chose que les traits ou les classes abstraites aideraient normalement à éviter.
virtualeyes
7

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

Évidemment, vous pouvez utiliser une hiérarchie et des correspondances plus sophistiquées, mais j'espère que cela vous donnera une idée. La clé est de tirer parti des extracteurs imbriqués fournis par les classes de cas


la source
3
Cela semble être la seule réponse ici qui n'a vraiment pas de champs en double
Alan Thomas