Как использовать классы case, когда требуется иерархия?

Я знаю, что вам не разрешено наследовать классы case, но как вы это сделаете, когда вам действительно нужно? У нас есть два класса в иерархии, оба содержат много полей, и мы должны иметь возможность создавать экземпляры обоих. Здесь мои варианты:

  • Если бы я сделал суперкласс обычным классом вместо класса case, я потерял бы все классность класса case, например toString, equals, методы hashCode и т.д.
  • Если я сохраню его как класс case, я бы нарушил правило не наследования классов case.
  • Если я использую композицию в дочернем классе - мне придется писать много методов и перенаправлять их на другой класс - это будет означать много работы и будет чувствовать себя нескалистым.

Что мне делать? Разве это не общая проблема?

Ответы

Ответ 1

Да, это довольно повторяющаяся проблема, я бы предложил создать черту со всеми родительскими свойствами, создать класс case, который только реализует его, а затем другой, который наследует его с большим количеством свойств.

sealed trait Parent {
  /* implement all common properties */
}

case class A extends Parent

case class B extends Parent {
  /*add all stuff you want*/
}

Хорошим способом увидеть это дерево, черты - это узлы, а классы case - листья.

Вы можете использовать признак или абстрактный класс в зависимости от ваших потребностей родителя. Однако избегайте использования класса, потому что вы сможете создавать его экземпляры, которые не будут изящными.

РЕДАКТИРОВАТЬ: Как было предложено в комментариях, вы можете запечатать этот признак, чтобы иметь исключения при компиляции, если не все классы case рассматриваются в сопоставлении с образцом. Это, например, описано в главе 15.5 "Программирование в Scala"

Ответ 2

Как насчет замены наследования делегацией?

Если ваши два класса в иерархии имеют много общих полей, то делегирование может уменьшить количество кода шаблона? Например:

case class Something(aa: A, bb: B, cc: C, payload: Payload)

sealed abstract class Payload

case class PayloadX(xx: X) extends Payload
case class PayloadY(yy: Y) extends Payload

И тогда вы создадите такие экземпляры Something:

val sth1 = Something('aa', 'bb', 'cc', PayloadX('xx'))
val sth2 = Something('aa', 'bb', 'cc', PayloadY('yy'))

И вы можете выполнить сопоставление с образцом:

sth1 match {
  case Something(_, _, _, PayloadX(_)) => ...
  case Something(_, _, _, PayloadY(_)) => ...
}

Преимущества: (?)

  • Когда вы объявляете PayloadX и PayloadY, вам не нужно повторять все поля в "Кое-что".

  • При создании экземпляров Something(... Payload(..)) вы можете повторно использовать код, создающий Something, как при создании Something(... PayloadX(..)), так и ...PayloadY.

Недостатки: (?)

  • Возможно, PayloadX и Y в вашем случае являются истинными подклассами Something, я имею в виду, что в вашем случае, возможно, делегирование семантически неверно?

  • Вам нужно написать something.payload.whatever вместо просто something.whatever (я полагаю, это может быть хорошо или плохо в зависимости от вашего конкретного случая?)

Ответ 3

Я также изучил эту проблему, AFAIK, лучшее, что вы собираетесь получить:

Разделяйте каждый класс случая от общего признака, который определяет абстрактный свойства каждого класса класса должны реализовывать

Он не удаляет шаблон (вообще), но определяет контракт, к которому должны придерживаться классы вашего дела, но не потерять набор функций класса событий...