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?
źródło
Odpowiedzi:
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
źródło
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
Person
osób (osób). Zasadniczo to właśnieOption
robi 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ą.
źródło
def name
cecha musi byćval name
. Mój kompilator dawał mi ostrzeżenia o nieosiągalnym kodzie z tym pierwszym.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
źródło
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.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