Herencia de clases de casos de Scala

89

Tengo una aplicación basada en Squeryl. Defino mis modelos como clases de casos, principalmente porque encuentro conveniente tener métodos de copia.

Tengo dos modelos que están estrictamente relacionados. Los campos son los mismos, muchas operaciones son comunes y deben almacenarse en la misma tabla de base de datos. Pero hay algún comportamiento que solo tiene sentido en uno de los dos casos, o que tiene sentido en ambos casos pero es diferente.

Hasta ahora solo he usado una única clase de caso, con una bandera que distingue el tipo de modelo, y todos los métodos que difieren según el tipo de modelo comienzan con un if. Esto es molesto y no del todo seguro.

Lo que me gustaría hacer es factorizar el comportamiento y los campos comunes en una clase de caso ancestro y que los dos modelos reales hereden de ella. Pero, hasta donde tengo entendido, heredar de clases de casos está mal visto en Scala, e incluso está prohibido si la subclase es en sí misma una clase de casos (no es mi caso).

¿Cuáles son los problemas y las trampas que debería tener en cuenta al heredar de una clase de caso? ¿Tiene sentido en mi caso hacerlo?

Andrea
fuente
1
¿No podrías heredar de una clase que no sea un caso o extender un rasgo común?
Eduardo
No estoy seguro. Los campos se definen en el ancestro. Quiero obtener métodos de copia, igualdad, etc. basados ​​en esos campos. Si declaro el padre como una clase abstracta y los hijos como una clase de caso, ¿tomará en cuenta los parámetros definidos en el padre?
Andrea
Creo que no, tienes que definir accesorios tanto en el padre abstracto (o rasgo) como en la clase de caso de destino. Al final, mucho es repetitivo, pero al menos escriba seguro
virtualeyes

Respuestas:

119

Mi forma preferida de evitar la herencia de clases de casos sin duplicación de código es algo obvia: crear una clase base común (abstracta):

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 desea ser más detallado, agrupe las propiedades en rasgos individuales:

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
fuente
83
¿Dónde está ese "sin duplicación de código" del que hablas? Sí, un contrato se define entre la clase de caso y su (s) padre (s), pero todavía está escribiendo props X2
virtualeyes
5
@virtualeyes Es cierto, todavía tienes que repetir las propiedades. Sin embargo, no es necesario que repita los métodos, que suelen tener más código que las propiedades.
Malte Schwerhoff
1
sí, solo esperaba evitar la duplicación de propiedades; otra respuesta sugiere clases de tipos como una posible solución; Sin embargo, no estoy seguro de cómo parece más orientado a la mezcla de comportamientos, como rasgos, pero más flexible. Simplemente repetitivo re: clases de casos, puedo vivir con él, sería bastante increíble si fuera de otra manera, realmente podría hackear grandes franjas de definiciones de propiedad
virtualeyes
1
@virtualeyes Estoy completamente de acuerdo en que sería genial si se pudiera evitar la repetición de propiedades de una manera fácil. Un complemento del compilador seguramente podría hacer el truco, pero no lo llamaría de una manera fácil.
Malte Schwerhoff
13
@virtualeyes Creo que evitar la duplicación de código no se trata solo de escribir menos. Para mí, se trata más de no tener la misma pieza de código en diferentes partes de su aplicación sin ninguna conexión entre ellas. Con esta solución, todas las subclases están vinculadas a un contrato, por lo que si la clase principal cambia, el IDE podrá ayudarlo a identificar las partes del código que necesita corregir.
Daniel
40

Dado que este es un tema interesante para muchos, permítanme arrojar algo de luz aquí.

Podrías optar por el siguiente enfoque:

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

Sí, tienes que duplicar los campos. Si no lo hace, simplemente no sería posible implementar la igualdad correcta entre otros problemas .

Sin embargo, no es necesario duplicar métodos / funciones.

Si la duplicación de algunas propiedades es tan importante para usted, utilice clases regulares, pero recuerde que no se ajustan bien a FP.

Alternativamente, puede usar la composición en lugar de la herencia:

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

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

La composición es una estrategia válida y sólida que también debes considerar.

Y en caso de que se pregunte qué significa un rasgo sellado, es algo que solo se puede extender en el mismo archivo. Es decir, las dos clases de casos anteriores deben estar en el mismo archivo. Esto permite realizar comprobaciones exhaustivas del compilador:

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

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

Da un error:

warning: match is not exhaustive!
missing combination            Tourist

Lo que es realmente útil. Ahora no te olvidarás de tratar con los otros tipos de Persons (personas). Esto es esencialmente lo que hace la Optionclase en Scala.

Si eso no le importa, puede hacerlo sin sellar y colocar las clases de casos en sus propios archivos. Y quizás ir con la composición.

Kai Sellgren
fuente
1
Creo que el def namerasgo in the debe ser val name. Mi compilador me estaba dando advertencias de código inalcanzable con el primero.
BAR
13

las clases de casos son perfectas para objetos de valor, es decir, objetos que no cambian ninguna propiedad y se pueden comparar con iguales.

Pero implementar iguales en presencia de herencia es bastante complicado. Considere dos clases:

class Point(x : Int, y : Int)

y

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

Entonces, de acuerdo con la definición, el ColorPoint (1,4, rojo) debería ser igual al Punto (1,4), son el mismo Punto después de todo. Entonces, ColorPoint (1,4, azul) también debería ser igual a Point (1,4), ¿verdad? Pero, por supuesto, ColorPoint (1,4, rojo) no debería ser igual a ColorPoint (1,4, azul), porque tienen colores diferentes. Ahí lo tienes, una propiedad básica de la relación de igualdad se rompe.

actualizar

Puede usar la herencia de rasgos para resolver muchos problemas como se describe en otra respuesta. Una alternativa aún más flexible es a menudo utilizar clases de tipos. Consulte ¿ Para qué son útiles las clases de tipos en Scala? o http://www.youtube.com/watch?v=sVMES4RZF-8

Jens Schauder
fuente
Entiendo y estoy de acuerdo con eso. Entonces, ¿qué sugieres que se debe hacer cuando tienes una aplicación que se ocupa, por ejemplo, de empleadores y empleados? Suponga que comparten todos los campos (nombre, dirección, etc.), con la única diferencia en algunos métodos; por ejemplo, uno puede querer definir Employer.fire(e: Emplooyee)pero no al revés. Me gustaría hacer dos clases diferentes, ya que en realidad representan objetos diferentes, pero tampoco me gusta la repetición que surge.
Andrea
¿Tienes un ejemplo de enfoque de clase de tipo con la pregunta aquí? es decir, en lo que respecta a las clases de casos
virtualeyes
@virtualeyes Uno podría tener tipos completamente independientes para los diversos tipos de Entidades y proporcionar Clases de Tipo para proporcionar el comportamiento. Estas clases de tipos podrían usar la herencia tanto como útiles, ya que no están vinculadas por el contrato semántico de las clases de casos. ¿Sería útil en esta pregunta? No sé, la pregunta no es lo suficientemente específica como para contarla.
Jens Schauder
@JensSchauder parece que los rasgos proporcionan lo mismo en términos de comportamiento, pero menos flexibles que las clases de tipos; Estoy llegando a la no duplicación de las propiedades de las clases de casos, algo que los rasgos o las clases abstractas normalmente ayudarían a evitar.
virtualeyes
7

En estas situaciones, tiendo a usar la composición en lugar de la herencia, es decir

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

Obviamente, puede usar una jerarquía y coincidencias más sofisticadas, pero es de esperar que esto le dé una idea. La clave es aprovechar los extractores anidados que proporcionan las clases de casos


fuente
3
Esta parece ser la única respuesta aquí que realmente no tiene campos duplicados
Alan Thomas