Вопрос или проблема
Смешение классов данных и объектов в иерархической структуре: как переопределить только toString() объекта, а не классов данных? [закрыто]
Я работаю с кодовой базой, которая использует иерархию для управления типами ошибок.
Существует базовый интерфейс ошибки, затем есть более широкие категориальные запечатанные классы для таких вещей, как ValidationErrors и DatabaseErrors. Каждому из этих запечатанных классов соответствуют свои более специфические ошибки, такие как NotFoundError под DatabaseErrors или InvalidDateFormatError под ValidationErrors.
Большинство из этих специфических ошибок – это классы данных и принимают такие вещи, как пойманное исключение или поле ввода. Некоторые ошибки, однако, не требуют никаких предоставленных значений, грубый пример может быть NameRequiredError. Они определяются как объекты, поскольку для класса данных требуется хотя бы один параметр конструктора.
Вот примерно как структура выглядит в коде в упрощённом и независимом примере. Реальная кодовая база значительно более сложная.
interface Error {
val message: String,
val createdAt: Instant
}
sealed class ValidationError : Error {
override val createdAt: Instant = Instant.now()
data class InvalidDateFormatError(val providedValue: String) : ValidationError() {
override val message: String = "'$providedValue' не соответствует стандартному формату yyyy-MM-dd"
}
object NameRequiredError : ValidationError() {
override val message = "Имя - обязательное поле"
}
}
Классы данных по умолчанию имеют очень удобные встроенные toString() и equals()/hashcode(). Это полезно для простых сравнений в модульных тестах, а также для составления списка всех ошибок в логе.
Основная проблема, с которой я сталкиваюсь, заключается в том, что объектные ошибки не имеют этих встроенных преимуществ. Вместо того чтобы получить InvalidDateFormatError(message="555555-5-555" не соответствует стандартному формату yyyy-MM-dd)
в строке, я получаю com.example.ValidationError.NameRequiredError@328ac25f
. Я выбрал объект вместо класса, потому что объекты можно было бы сравнивать в модульных тестах более успешно и последовательно, чем с использованием класса. Но у меня нет решения для toString() объекта.
Я могу добавить override fun toString(): String { ... }
к интерфейсу или запечатанному классу, но тогда это переопределит toString() для всего, и я в конечном итоге потеряю некоторую конкретную информацию в своих классах данных. Единым решением, которое я могу придумать, является добавление переопределения toString() только к самим объектам, но это добавит много шаблонного кода в мою иерархию, так как у меня довольно много таких. Это тоже не кажется хорошим решением.
Мои вопросы/беспокойства:
- Есть ли способ переопределить только объекты, а не классы данных?
- Существует ли другой способ добиться этой согласованности в функциональности toString() без добавления большого количества шаблонного кода?
- Есть ли что-то, кроме объекта, что я должен использовать вместо этого для ошибок без параметров, что не убрало бы иерархическую ценность из запечатанных классов и интерфейса?
- Мой подход к иерархии ошибок принципиально ошибочен и его следует реализовать иначе?
Ответ или решение
Ваша проблема с периодичным переопределением метода toString()
в иерархии ошибок является достаточно распространенной, особенно при работе с комбинированными классами данных и объектами в языках, поддерживающих объектно-ориентированное программирование, таких как Kotlin. Давайте рассмотрим ваши вопросы и предложим возможные решения.
-
Как переопределить
toString()
только для объектов и не для классов данных?Это можно сделать, добавляя переопределение
toString()
только непосредственно в каждом объекте, который вы определили в вашей иерархии. Это действительно добавляет некоторый объем повторного кода, но вы можете уменьшить этот объем, используя функцию, которая будет возвращать вашу строку. Например, для объектаNameRequiredError
можно сделать следующее:object NameRequiredError : ValidationError() { override val message = "Name is a required field" override fun toString(): String { return "NameRequiredError(message='$message')" } }
Вместо повторного кода вы можете создать функцию, которая будет генерировать строку по шаблону, и использовать её:
sealed class ValidationError : Error { override val createdAt: Instant = Instant.now() open fun getDescription(): String { return "${this::class.simpleName}(message='$message')" } } object NameRequiredError : ValidationError() { override val message = "Name is a required field" override fun toString() = getDescription() }
-
Есть ли способ достичь единообразия в
toString()
без большого объема кода?Как уже упоминалось, вы можете использовать базовую функцию
getDescription()
в вашемsealed class
, чтобы сократить дублирование кода. Это позволит вам иметь единый подход к генерации строкового представления для всех ошибок, не теряя при этом особенности классов данных. -
Есть ли что-то другое, что я мог бы использовать вместо объекта для ошибок без параметров?
В зависимости от вашего конкретного случая, вы можете использовать более сложные структуры, такие как классы-одиночки (singleton classes), если хотите сохранить поддержку экземпляров, или просто использовать обычные классы, если хотите, чтобы они не сохраняли состояние. Однако объекты имитируют поведение, характерное для ошибок, и сохраняют четкость и краткость в определении.
-
Не является ли мой подход к иерархии ошибок принципиально ошибочным и следует ли делать это иначе?
Ваш подход к иерархии ошибок является разумным. Использоватьsealed classes для представления различных категорий ошибок – это полезный и распространенный паттерн проектирования. Если ваши ошибки имеют общие черты и вы можете их организовать в иерархию, стоит продолжать следовать этому пути. Главное – следить за читаемостью и поддерживаемостью кода, что также подразумевает минимизацию дублирования.
В конечном итоге соблюдение единообразия и читаемости кода всегда должно быть вашим приоритетом. Используйте предложенные методы для управления вашими исключениями и значениями ошибок, и это позволит вам создать понятную и удобную для работы иерархию ошибок.