Как использовать классы 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, лучшее, что вы собираетесь получить:
Разделяйте каждый класс случая от общего признака, который определяет абстрактный свойства каждого класса класса должны реализовывать
Он не удаляет шаблон (вообще), но определяет контракт, к которому должны придерживаться классы вашего дела, но не потерять набор функций класса событий...