Наследование класса case в Scala

89

У меня есть приложение на базе Squeryl. Я определяю свои модели как классы case, в основном потому, что мне удобно иметь методы копирования.

У меня есть две модели, которые строго связаны. Поля совпадают, многие операции являются общими, и они должны храниться в одной таблице БД. Но есть поведение, которое имеет смысл только в одном из двух случаев или имеет смысл в обоих случаях, но отличается.

До сих пор я использовал только один класс case с флагом, который определяет тип модели, и все методы, которые различаются в зависимости от типа модели, начинаются с if. Это раздражает и не совсем безопасен.

Что я хотел бы сделать, так это разложить на множители общее поведение и поля в классе-предке и унаследовать от него две фактические модели. Но, насколько я понимаю, наследование от классов case не одобряется в Scala и даже запрещено, если подкласс сам является классом case (не в моем случае).

Какие проблемы и подводные камни я должен знать при наследовании от класса case? Имеет ли смысл в моем случае это делать?

Андреа
источник
1
Не могли бы вы унаследовать от класса без регистра или расширить общую черту?
Эдуардо
Я не уверен. Поля определены в предке. Я хочу получить методы копирования, равенство и т.д. на основе этих полей. Если я объявлю родительский класс абстрактным классом, а дочерние элементы - классом case, будут ли учитываться параметры учетных записей, определенные в родительском классе?
Андреа
Я думаю, что нет, вам нужно определить реквизиты как в абстрактном родительском (или трейт), так и в целевом классе case. В конце концов, много шаблонов, но, по крайней мере, безопасных типов
virtualeyes

Ответы:

119

Мой предпочтительный способ избежать наследования класса 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
где это "без дублирования кода", о котором вы говорите? Да, контракт определен между классом case и его родителями, но вы все еще набираете props X2
virtualeyes
5
@virtualeyes Правда, повторить свойства все равно придется. Однако вам не нужно повторять методы, которые обычно составляют больше кода, чем свойства.
Malte Schwerhoff
1
да, просто надеялся обойти дублирование свойств - другой ответ намекает на классы типов в качестве возможного обходного пути; не уверен, как, однако, кажется, более приспособленным к смешиванию в поведении, например чертам характера, но более гибким. Просто шаблонные классы re: case, могут жить с этим, было бы довольно невероятно, если бы было иначе, действительно можно было бы вырезать отличные наборы определений свойств
virtualeyes
1
@virtualeyes Я полностью согласен с тем, что было бы здорово, если бы можно было легко избежать повторения свойств. Плагин компилятора, безусловно, мог бы помочь, но я бы не назвал это легким способом.
Malte Schwerhoff 03
13
@virtualeyes Я думаю, что избежать дублирования кода - это не только меньше писать. Для меня это больше связано с отсутствием одного и того же фрагмента кода в разных частях вашего приложения без какой-либо связи между ними. В этом решении все подклассы привязаны к контракту, поэтому, если родительский класс изменится, IDE сможет помочь вам определить части кода, которые необходимо исправить.
Daniel
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)

Композиция - это действенная и разумная стратегия, которую вы также должны учитывать.

И если вам интересно, что означает запечатанная черта - это то, что может быть расширено только в том же файле. То есть два вышеуказанных класса case должны находиться в одном файле. Это позволяет выполнять исчерпывающие проверки компилятора:

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

Что действительно полезно. Теперь вы не забудете иметь дело с другими типами Personлюдей. По сути, это то, что Optionделает класс в Scala.

Если для вас это не имеет значения, вы можете сделать его незапечатанным и поместить классы case в их собственные файлы. И, возможно, пойти с композицией.

Кай Селлгрен
источник
1
Я думаю, что def nameчерта характера должна быть val name. Мой компилятор выдавал мне предупреждения о недоступности кода с первым.
BAR
13

Классы case идеально подходят для объектов значений, то есть объектов, которые не изменяют никаких свойств и могут быть сравнены с равными.

Но реализовать равные при наличии наследования довольно сложно. Рассмотрим два класса:

class Point(x : Int, y : Int)

а также

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

Итак, согласно определению ColorPoint (1,4, красный) должен быть равен Point (1,4), в конце концов, это одна и та же точка. Значит, ColorPoint (1,4, синий) тоже должен быть равен Point (1,4), верно? Но, конечно, ColorPoint (1,4, красный) не должен равняться ColorPoint (1,4, синий), потому что они имеют разные цвета. Итак, одно основное свойство отношения равенства нарушено.

Обновить

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

Йенс Шаудер
источник
Я понимаю и согласен с этим. Итак, что вы предлагаете делать, когда у вас есть приложение, которое касается, скажем, работодателей и сотрудников. Предположим, что они разделяют все поля (имя, адрес и т. Д.), Единственная разница заключается в некоторых методах - например, один может захотеть определить, Employer.fire(e: Emplooyee)но не наоборот. Я хотел бы создать два разных класса, поскольку они на самом деле представляют разные объекты, но мне также не нравится возникающее повторение.
Андреа
получил пример подхода к классу типа с вопросом здесь? то есть в отношении кейс-классов
virtualeyes
@virtualeyes Можно иметь полностью независимые типы для различных типов сущностей и предоставлять классы типов для обеспечения поведения. Эти классы типов могут использовать наследование в той мере, в какой они полезны, поскольку они не связаны семантическим контрактом классов case. Было бы полезно в этом вопросе? Не знаю, вопрос недостаточно конкретный, чтобы сказать.
Йенс Шаудер
@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
Похоже, это единственный ответ, который действительно не имеет повторяющихся полей
Алан Томас