Ereditarietà della classe case Scala


89

Ho un'applicazione basata su Squeryl. Definisco i miei modelli come classi case, soprattutto perché trovo conveniente avere metodi di copia.

Ho due modelli strettamente correlati. I campi sono gli stessi, molte operazioni sono in comune e devono essere memorizzate nella stessa tabella DB. Ma c'è un comportamento che ha senso solo in uno dei due casi, o che ha senso in entrambi i casi ma è diverso.

Finora ho utilizzato una sola classe case, con un flag che distingue il tipo di modello, e tutti i metodi che differiscono in base al tipo di modello iniziano con un if. Questo è fastidioso e non del tutto sicuro per i tipi.

Quello che vorrei fare è fattorizzare il comportamento e i campi comuni in una classe caso antenato e far ereditare i due modelli effettivi da esso. Ma, per quanto ne so, l'ereditarietà da classi case è disapprovata in Scala, ed è persino proibita se la sottoclasse è essa stessa una classe case (non il mio caso).

Quali sono i problemi e le insidie ​​di cui dovrei essere consapevole nell'ereditare da una classe case? Ha senso nel mio caso farlo?


1
Non potresti ereditare da una classe senza maiuscole o estendere un tratto comune?
Eduardo

Non sono sicuro. I campi sono definiti nell'antenato. Voglio ottenere metodi di copia, uguaglianza e così via basati su quei campi. Se dichiaro il genitore come una classe astratta e i figli come una classe case, prenderà in considerazione i parametri definiti sul genitore?
Andrea

Penso di no, devi definire oggetti di scena sia nel genitore astratto (o tratto) che nella classe del caso di destinazione. Alla fine, lot's o 'boilerplate, ma scrivi almeno safe
virtualeyes

Risposte:


119

Il mio modo preferito per evitare l'ereditarietà della classe case senza duplicazione del codice è piuttosto ovvio: creare una classe base comune (astratta):

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


Se vuoi essere più fine, raggruppa le proprietà in tratti individuali:

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
dov'è questo "senza duplicazione del codice" di cui parli? Sì, viene definito un contratto tra la classe case e i suoi genitori, ma stai ancora digitando props X2
virtualeyes

5
@virtualeyes Vero, devi ancora ripetere le proprietà. Non è necessario ripetere i metodi, tuttavia, che di solito ammontano a più codice rispetto alle proprietà.
Malte Schwerhoff

1
sì, speravo solo di aggirare la duplicazione delle proprietà - un'altra risposta suggerisce le classi di tipo come possibile soluzione alternativa; non sono sicuro di come, tuttavia, sembri più orientato a mescolare comportamenti, come i tratti, ma più flessibile. Solo boilerplate re: case classes, può conviverci, sarebbe piuttosto incredibile se fosse altrimenti, potrebbe davvero hackerare grandi quantità di definizioni di proprietà
virtualeyes

1
@virtualeyes Sono completamente d'accordo sul fatto che sarebbe fantastico se la ripetizione della proprietà potesse essere evitata in modo semplice. Un plug-in del compilatore potrebbe sicuramente fare il trucco, ma non lo definirei un modo semplice.
Malte Schwerhoff

13
@virtualeyes Penso che evitare la duplicazione del codice non sia solo scrivere di meno. Per me, si tratta più di non avere lo stesso pezzo di codice in parti diverse dell'applicazione senza alcuna connessione tra di loro. Con questa soluzione, tutte le sottoclassi sono legate a un contratto, quindi se la classe genitore cambia, l'IDE sarà in grado di aiutarti a identificare le parti del codice che devi correggere.
Daniel

40

Poiché questo è un argomento interessante per molti, permettetemi di fare un po 'di luce qui.

Potresti seguire il seguente approccio:

// 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ì, devi duplicare i campi. Se non lo fai, semplicemente non sarebbe possibile implementare la corretta uguaglianza tra gli altri problemi .

Tuttavia, non è necessario duplicare metodi / funzioni.

Se la duplicazione di alcune proprietà è così importante per te, usa classi regolari, ma ricorda che non si adattano bene a FP.

In alternativa, puoi usare la composizione invece dell'ereditarietà:

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

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

La composizione è una strategia valida e valida che dovresti anche considerare.

E nel caso ti chiedessi cosa significhi un tratto sigillato, è qualcosa che può essere esteso solo nello stesso file. Cioè, le due classi di casi precedenti devono essere nello stesso file. Ciò consente controlli esaustivi del compilatore:

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

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

Fornisce un errore:

warning: match is not exhaustive!
missing combination            Tourist

Che è davvero utile. Ora non dimenticherai di occuparti degli altri tipi di Personpersone. Questo è essenzialmente ciò che fa la Optionclasse in Scala.

Se questo non ti interessa, puoi renderlo non sigillato e lanciare le classi del caso nei loro file. E forse vai con la composizione.


1
Penso che il def nametratto debba essere val name. Il mio compilatore mi dava avvisi di codice irraggiungibili con il primo.
BAR

13

le classi case sono perfette per gli oggetti valore, cioè gli oggetti che non cambiano alcuna proprietà e possono essere confrontati con uguali.

Ma implementare la parità in presenza di eredità è piuttosto complicato. Considera due classi:

class Point(x : Int, y : Int)

e

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

Quindi secondo la definizione il ColorPoint (1,4, rosso) dovrebbe essere uguale al punto (1,4) sono lo stesso punto dopotutto. Quindi ColorPoint (1,4, blu) dovrebbe essere uguale a Point (1,4), giusto? Ma ovviamente ColorPoint (1,4, rosso) non dovrebbe essere uguale a ColorPoint (1,4, blu), perché hanno colori diversi. Ecco fatto, una proprietà fondamentale della relazione di uguaglianza è interrotta.

aggiornare

Puoi utilizzare l'ereditarietà dai tratti risolvendo molti problemi come descritto in un'altra risposta. Un'alternativa ancora più flessibile è spesso quella di utilizzare classi di tipo. Vedi A cosa servono le classi di tipo in Scala? o http://www.youtube.com/watch?v=sVMES4RZF-8


Lo capisco e sono d'accordo. Quindi, cosa suggerisci di fare quando hai un'applicazione che si occupa, ad esempio, di datori di lavoro e dipendenti. Supponiamo che condividano tutti i campi (nome, indirizzo e così via), con l'unica differenza in alcuni metodi, ad esempio uno potrebbe voler definire Employer.fire(e: Emplooyee)ma non il contrario. Vorrei creare due classi diverse, poiché in realtà rappresentano oggetti diversi, ma non mi piace anche la ripetizione che ne deriva.
Andrea

hai un esempio di approccio di classe tipo con la domanda qui? vale a dire per quanto riguarda le classi di casi
virtualeyes

@virtualeyes Si potrebbero avere tipi completamente indipendenti per i vari tipi di entità e fornire classi di tipi per fornire il comportamento. Queste classi di tipo potrebbero utilizzare l'ereditarietà tanto quanto utile, poiché non sono vincolate dal contratto semantico delle classi case. Sarebbe utile in questa domanda? Non lo so, la domanda non è abbastanza specifica da poterlo dire.
Jens Schauder

@ JensSchauder sembrerebbe che i tratti forniscano la stessa cosa in termini di comportamento, solo meno flessibili delle classi di tipo; Sto arrivando alla non duplicazione delle proprietà della classe case, qualcosa che i tratti o le classi astratte normalmente aiuterebbero a evitare.
virtualeyes

7

In queste situazioni tendo a usare la composizione invece dell'ereditarietà, cioè

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

Ovviamente puoi usare una gerarchia e corrispondenze più sofisticate, ma si spera che questo ti dia un'idea. La chiave è sfruttare gli estrattori annidati forniti dalle classi case


3
Questa sembra essere l'unica risposta qui che davvero non ha campi duplicati
Alan Thomas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.