Есть ли у вас сценарии, в которых необходимо выполнять различную обработку в зависимости от статуса процесса? Что-то типа:

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, не стесняйтесь использовать мою реферальную ссылку! Вам также могут понравиться эти футболки с дизайном, вдохновленным кодом.