Наслідування класу справи Scala


88

У мене є додаток, заснований на Squeryl. Я визначаю свої моделі як кейси, здебільшого тому, що мені зручно мати методи копіювання.

У мене є дві моделі, які суворо пов’язані. Поля однакові, багато операцій спільні, і їх слід зберігати в одній таблиці БД. Але є така поведінка, яка має сенс лише в одному з двох випадків, або яка має сенс в обох випадках, але відрізняється.

Дотепер я використовував лише один клас case, із позначкою, що відрізняє тип моделі, а всі методи, що відрізняються залежно від типу моделі, починаються з if. Це дратує і не зовсім безпечно.

Що я хотів би зробити, це врахувати загальну поведінку та поля класу справи предків і мати дві фактичні моделі, які успадковуються від нього. Але, наскільки я розумію, наслідування з класів кейсів заборонено в Scala і навіть заборонено, якщо сам підклас є класом кейсу (не мій випадок).

Які проблеми та підводні камені мені слід пам’ятати при успадкуванні від класу справи? Чи є сенс у моєму випадку це робити?


1
Чи не могли б ви успадкувати клас, що не є регістром, або розширити загальну рису?
Едуардо

Я не впевнений. Поля визначені в предок. Я хочу отримати методи копіювання, рівність тощо на основі цих полів. Якщо я оголошу батька абстрактним класом, а дітей - класом регістру, чи буде він враховувати параметри обліку, визначені батьківським?
Андреа

Я думаю, що ні, вам потрібно визначити реквізит як у абстрактному батьківському (або означному), так і в цільовому класах. Врешті-решт, партія типова, але принаймні тип безпечний
virtualeyes

Відповіді:


118

Мій найкращий спосіб уникнення успадкування класу case без дублювання коду є дещо очевидним: створіть загальний (абстрактний) базовий клас:

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


Якщо ви хочете бути більш дрібним, згрупуйте властивості за окремими ознаками:

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
де це "без дублювання коду", про яке ви говорите? Так, контракт визначений між класом справи та його батьками, але ви все ще набираєте реквізит X2
virtualeyes

5
@virtualeyes Правда, вам все одно доведеться повторити властивості. Однак вам не потрібно повторювати методи, які зазвичай складають більше коду, ніж властивості.
Malte Schwerhoff,

1
так, я просто сподівався обійти дублювання властивостей - інша відповідь натякає на класи типу як можливе обхідне рішення; не впевнений, як, однак, здається більш орієнтованим на змішування поведінки, як риси, але більш гнучким. Просто типовий зразок: класи класів, можна жити з цим, було б досить неймовірним, якби було інакше, дійсно міг би зламати великі масиви визначень властивостей
virtualeyes

1
@virtualeyes Я повністю згоден, що було б чудово, якби повторення властивостей можна було уникнути простим способом. Плагін компілятора, безсумнівно, міг би зробити це, але я б не назвав це простим способом.
Malte Schwerhoff,

13
@virtualeyes Я думаю, що уникнення дублювання коду полягає не лише в тому, щоб писати менше. Для мене це більше про те, щоб не мати однаковий фрагмент коду в різних частинах вашої програми без жодного зв’язку між ними. За допомогою цього рішення всі підкласи пов’язані з контрактом, тому, якщо батьківський клас зміниться, IDE зможе допомогти вам визначити частини коду, які потрібно виправити.
Даніель

40

Оскільки це цікава тема для багатьох, дозвольте мені трохи пролити світло.

Ви можете використати такий підхід:

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

Так, вам потрібно дублювати поля. Якщо ви цього не зробите, просто неможливо буде застосувати правильну рівність серед інших проблем .

Однак вам не потрібно дублювати методи / функції.

Якщо дублювання кількох властивостей є для вас настільки важливим, тоді використовуйте звичайні класи, але пам’ятайте, що вони погано підходять для FP.

Як варіант, ви можете використовувати композицію замість успадкування:

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

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

Композиція - це дійсна і обґрунтована стратегія, яку ви також повинні врахувати.

І якщо вам цікаво, що означає запечатана риса - це те, що можна продовжити лише в одному файлі. Тобто, два наведені вище кейси повинні знаходитись в одному файлі. Це дозволяє проводити вичерпні перевірки компілятора:

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

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

Видає помилку:

warning: match is not exhaustive!
missing combination            Tourist

Що справді корисно. Тепер ви не забудете мати справу з іншими типами Persons (людей). Це, по суті, те, що робить Optionклас у Scala.

Якщо це для вас не має значення, ви можете зробити це непломбованим і передати класи справи у власні файли. І, можливо, піти з композицією.


1
Я думаю, що def nameв рисі має бути val name. Мій компілятор давав мені недоступні попередження коду з першим.
БАР

13

Класи case ідеально підходять для об'єктів значення, тобто об'єктів, які не змінюють жодних властивостей, і їх можна порівняти з рівними.

Але реалізація рівних за наявності спадщини досить складна. Розглянемо два класи:

class Point(x : Int, y : Int)

і

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

Отже, згідно з визначенням ColorPoint (1,4, червоний) повинен дорівнювати Точці (1,4), вони зрештою однакові Точка. Тож ColorPoint (1,4, синій) також повинен дорівнювати точці (1,4), так? Але звичайно ColorPoint (1,4, червоний) не повинен дорівнювати ColorPoint (1,4, синій), оскільки вони мають різні кольори. Ось, одна основна властивість відношення рівності порушена.

оновлення

Ви можете використовувати успадкування від рис, що вирішують безліч проблем, як описано в іншій відповіді. Ще більш гнучкою альтернативою часто є використання класів типів. Див. Для чого корисні класи класів у Scala? або http://www.youtube.com/watch?v=sVMES4RZF-8


Я це розумію і погоджуюсь. Отже, що ви пропонуєте робити, коли у вас є програма, яка стосується, скажімо, роботодавців та працівників. Припустимо, що вони діляться усіма полями (ім’ям, адресою тощо), різниця лише в деяких методах - наприклад, один, можливо, захоче визначити, Employer.fire(e: Emplooyee)але не навпаки. Я хотів би зробити два різні класи, оскільки вони насправді представляють різні предмети, але мені також не подобається повторення, що виникає.
Андреа

отримали приклад підходу до класу типів із запитанням тут? тобто щодо класів справ
virtualeyes

@virtualeyes Можна мати цілком незалежні типи для різних типів Сутностей і надати Класи Типів для забезпечення поведінки. Ці класи класів можуть використовувати спадщину настільки ж корисно, оскільки вони не пов'язані семантичним контрактом класів випадків. Чи було б це корисно в цьому питанні? Не знаю, питання недостатньо конкретне, щоб сказати.
Йенс Шаудер,

@JensSchauder здається, що риси надають одне і те ж з точки зору поведінки, лише менш гнучкі, ніж класи класів; Я розумію не дублювання властивостей класу case, чогось, що риси або абстрактні класи зазвичай допомагають уникнути.
virtualeyes

7

У цих ситуаціях я, як правило, використовую композицію замість спадкування, тобто

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

Очевидно, що ви можете використовувати більш досконалу ієрархію та збіги, але, сподіваємось, це дає вам уявлення. Ключ полягає у використанні переваг вкладених екстракторів, які надають класи case


3
Це, здається, єдина відповідь тут, яка справді не має дублікатів полів
Алан Томас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.