Есть ли у вас сценарии, в которых необходимо выполнять различную обработку в зависимости от статуса процесса? Что-то типа:
data class Result( val isSuccess: Boolean, val value: String?, // non-null when isSuccess is true val errorMessage: String? // non-null when isSuccess is false ) private fun process(): Result { return ... // do something and return a Result } fun main() { val result = process() val text: String = if (result.isSuccess) { println(result.value!!) } else { println(result.errorMessage!!) } println(text) }
У этого подхода есть несколько недостатков:
- Потребитель
Result
всегда должен не забывать проверять атрибутisSuccess
перед его обработкой, иначе он может получить доступ к неправильному атрибуту. - Класс
Result
содержит объединение всех атрибутов всех возможных статусов (в данном случае успех или неудача). Невозможно узнать, какие атрибуты какому статусу принадлежат и какие атрибуты присутствуют. Придется использовать комментарии. - Атрибуты, не относящиеся к статусу, должны допускать значение NULL или иметь какое-то значение по умолчанию / фиктивное значение.
- Поскольку атрибуты допускают значение NULL, необходимо выполнить некоторую обработку NULL (т. Е. Нам все еще нужно обрабатывать NULL, даже если мы можем быть уверены, что, когда мы получим успешный результат, значение никогда не будет равно NULL)
Если мы немного расширимся, допустим, что Result
может иметь 3 типа статуса:
enum class Status { SUCCESS, FAILURE, PENDING } data class Result( val status: Status, val value: String?, // non-null when SUCCESS val errorMessage: String? // non-null when FAILURE ) private fun process(): Result { return ... // do something and return a Result } fun main() { val result = process() val text: String = when(result.status) { Status.SUCCESS -> result.value!! Status.FAILURE -> result.errorMessage!! Status.PENDING -> "no attributes are available" else -> "undefined status" } println(text) }
У этого недостатков еще больше:
- Теперь нам нужно определить перечисление
Status
для поддержки возможных статусов. - Нам нужно предложение
else
в условном выраженииwhen
, даже если мы знаем, что обработали все статусы, и предложениеelse
никогда не будет достигнуто.
Есть более простой и элегантный подход с использованием Запечатанных классов. Описание в документации довольно запутанное, поэтому, надеюсь, оно более ясное: Запечатанные классы - это набор родительских / дочерних классов, которые должны быть определены в одном файле, применяются обычные отношения суперкласс / подкласс и ограничения. Родительский класс определяется с помощью модификатора sealed
. Благодаря этому компилятор знает исчерпывающий список дочерних классов и может творить чудеса, как мы увидим. В результате дочерние классы имеют общие черты с перечислениями.
С помощью запечатанных классов мы можем внести следующие изменения в приведенный выше код.
- 1) Поскольку они действуют как перечисления, нам не нужен другой
enum class
. Мы можем определитьResult
какsealed class
, а затем представить статус как его дочерние элементы. - 2) Поскольку каждый статус является подклассом, он может определять атрибуты, специфичные для него самого, включая определение допустимости пустых значений.
sealed class Result data class Success(val value: String) : Result() data class Failure(val errorMessage: String) : Result() object Pending : Result()
- 3) Потребитель
Result
теперь уполномочен компилятором проверять статусы, в противном случае он имеет только классResult
без атрибутов - 4) Сама проверка упрощена, так как компилятор стал умнее - он знает полный список возможных статусов.
- 5) В Kotlin есть умные приведения, и в сочетании с предложением
when
мы можем получить доступ к атрибутам напрямую и без ненужных проверок на null. Что еще более важно, нет возможности случайно получить доступ к неправильному атрибуту.
val text: String = when(result) { is Success -> result.value is Failure -> result.errorMessage is Pending -> "no attributes are available" }
Плюсы
- Мы избавились от лишнего класса
Status
. Метаданные статуса теперь «закодированы» в самом типе. - Мы избавились от избыточных нулевых типов! Теперь также очень ясно, какой статус владеет какими атрибутами, и для потребителя сразу же очевидно, какие атрибуты доступны.
- Потребитель вынуждает компилятор проверять статус, у него нет возможности забыть или неправильно использовать атрибуты.
- Если мы добавим новый статус, компилятор заставит нас добавить его в предложение
when
. Раньше при использовании перечислений мы могли забыть проверить новый статус, потому что компилятору все равно! - Код немного менее подробный и немного более свободный. Сравните чтение
when result.status == Status.SUCCESS
иwhen result is Success
. - Поскольку каждый статус представляет собой класс сам по себе, мы можем определить для него разные функции или реализовать другое поведение без дополнительных условных проверок статуса. (Например, не говорю, что это хорошая идея, но мы могли бы реализовать разные
toString()
для каждого статуса)
Полный код с закрытыми классами
sealed class Result data class Success(val value: String) : Result() data class Failure(val errorMessage: String) : Result() object Pending : Result() private fun process(): Result { return ... // do something and return a Result } fun main() { val result = process() val text: String = when(result) { is Success -> result.value is Failure -> result.errorMessage is Pending -> "no attributes are available" } println(text) }
Привет, если вам понравился этот пост, я подумал, что вы могли бы заинтересоваться членством в Medium, чтобы получить доступ к качественному контенту от авторов Medium, не стесняйтесь использовать мою реферальную ссылку! Вам также могут понравиться эти футболки с дизайном, вдохновленным кодом.