Dziedziczenie klas przypadków w skali Scala

89

Mam aplikację opartą na Squeryl. Moje modele definiuję jako klasy przypadków, głównie dlatego, że uważam za wygodne metody kopiowania.

Mam dwa modele, które są ze sobą ściśle powiązane. Pola są takie same, wiele operacji jest wspólnych i mają być przechowywane w tej samej tabeli DB. Ale jest pewne zachowanie, które ma sens tylko w jednym z dwóch przypadków lub ma sens w obu przypadkach, ale jest inne.

Do tej pory używałem tylko jednej klasy przypadku, z flagą odróżniającą typ modelu, a wszystkie metody, które różnią się w zależności od typu modelu, zaczynają się od if. Jest to denerwujące i niezupełnie bezpieczne.

To, co chciałbym zrobić, to wziąć pod uwagę typowe zachowanie i pola w klasie przypadku przodka i sprawić, by te dwa rzeczywiste modele odziedziczyły po nim. Ale, o ile rozumiem, dziedziczenie z klas przypadków jest źle widziane w Scali i jest nawet zabronione, jeśli podklasa sama jest klasą przypadku (nie mój przypadek).

Jakie są problemy i pułapki, na które powinienem zwrócić uwagę podczas dziedziczenia z klasy przypadku? Czy w moim przypadku ma to sens?

Andrea
źródło
1
Czy nie mógłbyś odziedziczyć z klasy bez przypadku lub rozszerzyć wspólną cechę?
Eduardo
Nie jestem pewien. Pola są zdefiniowane w przodku. Chcę uzyskać metody kopiowania, równość itd. W oparciu o te pola. Jeśli zadeklaruję rodzica jako klasę abstrakcyjną, a dzieci jako klasę przypadku, czy uwzględni parametry zdefiniowane w rodzicu?
Andrea
Myślę, że nie, musisz zdefiniować rekwizyty zarówno w abstrakcyjnej klasie nadrzędnej (lub cechy), jak i docelowej klasie przypadku. W końcu, dużo nie ma żadnych schematów, ale typ bezpieczny przynajmniej
virtualeyes

Odpowiedzi:

119

Mój preferowany sposób na uniknięcie dziedziczenia klas przypadków bez powielania kodu jest dość oczywisty: utwórz wspólną (abstrakcyjną) klasę bazową:

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


Jeśli chcesz być bardziej drobnoziarnisty, pogrupuj właściwości w poszczególne cechy:

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
źródło
83
gdzie jest to „bez powielania kodu”, o którym mówisz? Tak, umowa jest definiowana między klasą przypadku a jej rodzicami, ale nadal wpisujesz rekwizyty X2
virtualeyes
5
@virtualeyes Prawda, nadal musisz powtórzyć właściwości. Nie musisz jednak powtarzać metod, co zwykle oznacza więcej kodu niż właściwości.
Malte Schwerhoff
1
tak, chciałem tylko obejść powielanie właściwości - inna odpowiedź wskazuje na klasy typów jako możliwe obejście; nie jestem jednak pewien, jak wydaje się być bardziej nastawiony na mieszanie zachowań, takich jak cechy, ale bardziej elastyczny. Tylko standardowa tablica re: klasy przypadków, może z tym żyć, byłaby całkiem niewiarygodna, gdyby była inaczej, mogłaby naprawdę wykuć ogromne połacie definicji właściwości
virtualeyes
1
@virtualeyes Całkowicie się zgadzam, że byłoby wspaniale, gdyby można było w łatwy sposób uniknąć powtarzania właściwości. Wtyczka kompilatora z pewnością mogłaby załatwić sprawę, ale nie nazwałbym tego prostym sposobem.
Malte Schwerhoff
13
@virtualeyes Myślę, że unikanie powielania kodu to nie tylko pisanie mniej. Dla mnie bardziej chodzi o to, aby nie mieć tego samego fragmentu kodu w różnych częściach aplikacji bez żadnego połączenia między nimi. Dzięki temu rozwiązaniu wszystkie podklasy są powiązane z kontraktem, więc jeśli klasa nadrzędna ulegnie zmianie, IDE będzie w stanie pomóc w zidentyfikowaniu części kodu, które należy naprawić.
Daniel
40

Ponieważ dla wielu jest to interesujący temat, rzucę tutaj trochę światła.

Możesz zastosować następujące podejście:

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

Tak, musisz powielić pola. Jeśli tego nie zrobisz, po prostu nie będzie możliwe wdrożenie poprawnej równości wśród innych problemów .

Nie musisz jednak powielać metod / funkcji.

Jeśli powielenie kilku właściwości jest dla Ciebie tak ważne, użyj zwykłych klas, ale pamiętaj, że nie pasują one dobrze do FP.

Alternatywnie możesz użyć kompozycji zamiast dziedziczenia:

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

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

Kompozycja to ważna i rozsądna strategia, którą również powinieneś rozważyć.

A jeśli zastanawiasz się, co oznacza cecha zapieczętowana - jest to coś, co można rozszerzyć tylko w tym samym pliku. Oznacza to, że dwie powyższe klasy przypadków muszą znajdować się w tym samym pliku. Pozwala to na wyczerpujące sprawdzenie kompilatora:

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

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

Daje błąd:

warning: match is not exhaustive!
missing combination            Tourist

Co jest naprawdę przydatne. Teraz nie zapomnisz poradzić sobie z innymi typami Personosób (osób). Zasadniczo to właśnie Optionrobi klasa w Scali.

Jeśli to nie ma dla Ciebie znaczenia, możesz uczynić to niezamkniętym i wrzucić klasy spraw do ich własnych plików. I może idź z kompozycją.

Kai Sellgren
źródło
1
Myślę, że def namececha musi być val name. Mój kompilator dawał mi ostrzeżenia o nieosiągalnym kodzie z tym pierwszym.
BAR
13

klasy przypadków są idealne dla obiektów wartości, tj. obiektów, które nie zmieniają żadnych właściwości i można je porównać z równymi.

Jednak wdrażanie równości w przypadku dziedziczenia jest dość skomplikowane. Rozważ dwie klasy:

class Point(x : Int, y : Int)

i

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

Zatem zgodnie z definicją ColorPoint (1,4, czerwony) powinien być równy punktowi (1,4), w końcu są tym samym punktem. Więc ColorPoint (1,4, niebieski) powinien również być równy Point (1,4), prawda? Ale oczywiście ColorPoint (1,4, czerwony) nie powinien równać się ColorPoint (1,4, niebieski), ponieważ mają różne kolory. Proszę bardzo, jedna podstawowa właściwość relacji równości jest zerwana.

aktualizacja

Możesz użyć dziedziczenia po cechach, rozwiązując wiele problemów, jak opisano w innej odpowiedzi. Jeszcze bardziej elastyczną alternatywą jest często użycie klas typów. Zobacz, do czego przydatne są klasy typów w Scali? lub http://www.youtube.com/watch?v=sVMES4RZF-8

Jens Schauder
źródło
Rozumiem i zgadzam się z tym. Więc co sugerujesz, powinno być zrobione, gdy masz aplikację, która dotyczy, powiedzmy, pracodawców i pracowników. Załóżmy, że mają one wspólne wszystkie pola (imię i nazwisko, adres itd.), Jedyną różnicą są niektóre metody - na przykład można chcieć zdefiniować, Employer.fire(e: Emplooyee)ale nie odwrotnie. Chciałbym stworzyć dwie różne klasy, ponieważ w rzeczywistości reprezentują one różne obiekty, ale nie podoba mi się też powtarzalność, która się pojawia.
Andrea
masz przykład podejścia do klasy typów z tym pytaniem? tj. w odniesieniu do klas przypadków
virtualeyes
@virtualeyes Można by mieć całkowicie niezależne typy dla różnych typów jednostek i zapewnić klasy typów, aby zapewnić zachowanie. Te klasy typów mogą wykorzystywać dziedziczenie równie przydatne, ponieważ nie są związane semantycznym kontraktem klas przypadków. Czy byłoby to przydatne w tym pytaniu? Nie wiem, pytanie nie jest na tyle szczegółowe, aby powiedzieć.
Jens Schauder
@JensSchauder wydawałoby się, że cechy zapewniają to samo pod względem zachowania, tylko mniej elastyczne niż klasy typów; Dochodzę do tego, że nie powielają się właściwości klas przypadków, czyli coś, czego cechy lub klasy abstrakcyjne zwykle pomogłyby uniknąć.
virtualeyes
7

W takich sytuacjach używam raczej kompozycji zamiast dziedziczenia, np

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

Oczywiście możesz użyć bardziej wyrafinowanej hierarchii i dopasowań, ale mam nadzieję, że to daje ci pomysł. Kluczem jest wykorzystanie zagnieżdżonych ekstraktorów, które zapewniają klasy przypadków


źródło
3
Wydaje się, że jest to jedyna odpowiedź, która tak naprawdę nie ma zduplikowanych pól
Alan Thomas