ОБНОВЛЕНИЕ 2016/02/25:
Несмотря на то, что ответ, который я написал ниже, остается достаточным, стоит также сослаться на другой связанный с ним ответ относительно сопутствующего объекта класса случая.А именно, как точно воспроизвести сгенерированный компилятором неявный объект-компаньон , который возникает, когда определяется только сам класс case.Для меня это оказалось противоинтуитивно.
Сводка:
Вы можете изменить значение параметра класса дела перед его сохранением в классе дела довольнопросто пока он все еще остается действительным ADT (абстрактный тип данных).В то время как решение было относительно простым, выяснить подробности было немного сложнее.
Подробности:
Если вы хотите, чтобы когда-либо создавались только допустимые экземпляры вашего класса дел, что является важным допущением в ADT (абстрактный тип данных), существуетколичество вещей, которые вы должны сделать.
Например, сгенерированный компилятором метод copy
предоставляется по умолчанию для класса case.Таким образом, даже если вы очень внимательно следите за тем, чтобы с помощью метода apply
явного сопутствующего объекта создавались только экземпляры, которые гарантировали, что они могут содержать только значения верхнего регистра, следующий код создаст экземпляр класса case со значением нижнего регистра:1021 *
val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"
Кроме того, классы case реализуют java.io.Serializable
.Это означает, что ваша осторожная стратегия, предусматривающая использование только прописных букв, может быть нарушена с помощью простого текстового редактора и десериализации.
Итак, для всех различных способов использования вашего класса case (доброжелательно и / или злонамеренно), вот действия, которые вы должны предпринять:
- Для вашего явного сопутствующего объекта:
- Создайте его, используя точно такое же имя, как у вашего класса дел
- Имеется доступ к закрытым частям класса дел
- Создайте метод
apply
с точно такой же сигнатурой, что и у основного конструктора для вашего case-класса - Он будет успешно скомпилирован после выполнения шага 2.1
- Обеспечение реализации, получающей экземпляр класса caseиспользуя оператор
new
и предоставляя пустую реализацию {}
- Теперь это будет создавать экземпляр класса case строго на ваших условиях
- Должна быть предоставлена пустая реализация
{}
, поскольку класс caseобъявляется abstract
(см. шаг 2.1)
- Для класса вашего дела:
- Объявите его
abstract
- ПредотвращатьКомпилятор Scala генерирует метод
apply
в сопутствующем объекте, что и является причиной ошибки компиляции «метод определен дважды ...» (шаг 1.2 выше)
- Отметитьпервичный конструктор как
private[A]
- Первичный конструктор теперь доступен только для самого класса case и его сопутствующего объекта (тот, который мы определили выше на шаге 1.1)
- Создание
readResolve
метода - Предоставление реализации с использованием метода apply (шаг 1.2 выше)
- Создание
copy
метода - Определите, чтобы он имел точно такую же сигнатуру, что и основной конструктор класса наблюдения
- Для каждого параметра добавьте значение по умолчанию, используя то же имя параметра (например:
s: String = s
) - Обеспечьте реализацию, используяметод применения (шаг 1.2 ниже)
Вот ваш код, измененный с помощью вышеуказанных действий:
object A {
def apply(s: String, i: Int): A =
new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
А вот вашкод после реализации требованияuire (предложено в ответе @ollekullberg), а также определение идеального места для размещения любого вида кэширования:
object A {
def apply(s: String, i: Int): A = {
require(s.forall(_.isUpper), s"Bad String: $s")
//TODO: Insert normal instance caching mechanism here
new A(s, i) {} //abstract class implementation intentionally empty
}
}
abstract case class A private[A] (s: String, i: Int) {
private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
И эта версия более безопасна / надежна, если этот код будет использоваться через взаимодействие Java (скрываетсякласс case как реализация и создает конечный класс, который предотвращает деривации):
object A {
private[A] abstract case class AImpl private[A] (s: String, i: Int)
def apply(s: String, i: Int): A = {
require(s.forall(_.isUpper), s"Bad String: $s")
//TODO: Insert normal instance caching mechanism here
new A(s, i)
}
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
Хотя это прямо отвечает на ваш вопрос, есть еще больше способов расширить этот путь вокруг классов дел, помимо кэширования экземпляров. Для собственных нужд проекта я создал еще более обширное решение , которое я задокументировал на CodeReview (дочерний сайт StackOverflow). Если вы в конечном итоге просматриваете его, используете или используете мое решение, пожалуйста, оставьте мне свои отзывы, предложения или вопросы, и в разумных пределах я сделаю все возможное, чтобы ответить в течение дня.