Найти тему

Закрытые классы в Kotlin

Оглавление

1. Введение

Проще говоря, язык Kotlin позаимствовал ряд концепций из других функциональных языков, чтобы помочь в написании более безопасного и удобочитаемого кода. Одной из таких концепций являются закрытые иерархии.

2. Что такое закрытый класс?

Закрытые классы позволяют нам фиксировать иерархии типов и запрещать разработчикам создавать новые подклассы.

Они полезны, когда у нас очень строгая иерархия наследования, с определенным набором возможных подклассов и никакими другими.
Начиная с Kotlin 1.5, закрытые классы могут иметь подклассы во всех файлах одного и того же модуля компиляции и одного и того же пакета.

Закрытые классы также неявно абстрактны. Они должны рассматриваться как таковые во всем остальном нашем коде, за исключением того, что ничто другое не может их реализовать.

Закрытые классы могут иметь определенные в них поля и методы, включая как абстрактные, так и реализованные функции. Это означает, что мы можем иметь базовое представление класса, а затем корректировать его в соответствии с подклассами.

2.1. Герметичные интерфейсы

Начиная с Kotlin 1.5, интерфейсы также могут иметь модификатор sealed, который работает с интерфейсами так же, как и с классами: все реализации sealed интерфейса должны быть известны во время компиляции.

Одним из преимуществ закрытых интерфейсов перед закрытыми классами является возможность наследования от нескольких закрытых интерфейсов. Это невозможно для закрытых классов из-за отсутствия множественного наследования в Kotlin.

3. Когда использовать закрытые классы?

Закрытые классы предназначены для использования, когда существует очень специфический набор возможных опций для значения, и где каждая из этих опций функционально отличается – просто алгебраические типы данных.

Распространенные варианты использования могут включать реализацию конечного автомата или монадическое программирование, которое становится все более популярным с появлением концепций функционального программирования.

Всякий раз, когда у нас есть несколько вариантов, и они отличаются только значением данных, нам, возможно, лучше использовать вместо этого классы Enum.

Всякий раз, когда у нас есть неизвестное количество опций, мы не можем использовать закрытый класс, потому что это помешает нам добавлять опции за пределами исходного файла.

4. Написание закрытых классов

Давайте начнем с написания нашего собственного закрытого класса – хорошим примером такой закрытой иерархии является необязательный из Java 8, который может быть либо Some, либо None.

При реализации этого имеет большой смысл ограничить возможность создания новых реализаций – две предоставленные реализации являются исчерпывающими, и никто не должен добавлять свои собственные.

Таким образом, мы можем реализовать это:

sealed class Optional<out V> {
// ...
abstract fun isPresent(): Boolean
}

data class Some<out V>(val value: V) : Optional<V>() {
// ...
override fun isPresent(): Boolean = true
}

class None<out V> : Optional<V>() {
// ...
override fun isPresent(): Boolean = false
}

Теперь можно гарантировать, что каждый раз, когда у нас есть экземпляр Optional<V>, на самом деле у нас есть либо Some<V>, либо None<V>.

В Java 8 фактическая реализация выглядит иначе из-за отсутствия закрытых классов.

Затем мы можем использовать это в наших вычислениях:

val result: Optional<String> = divide(1, 0)
println(result.isPresent())
if (result is Some) {
println(result.value)
}

Первая строка вернет либо Some, либо None. Затем мы выводим, получили мы результат или нет.

5. Использование With When

Kotlin поддерживает использование закрытых классов в своих конструкциях when. Поскольку всегда существует точный набор возможных подклассов, компилятор может предупредить нас, если какая-либо ветвь не обрабатывается, точно так же, как это делается для перечислений.

Это означает, что в таких ситуациях обычно нет необходимости в универсальном обработчике, что, в свою очередь, означает, что добавление нового подкласса автоматически безопасно - компилятор немедленно предупредит нас, если мы не обработали его, и нам нужно будет исправить такие ошибки, прежде чем продолжить.

Приведенный выше пример может быть расширен для вывода либо результата ошибки, в зависимости от возвращаемого типа:

val message = when (result) {
is Some -> "Answer: ${result.value}"
is None -> "No result"
}
println(message)

Если бы какая-либо из двух ветвей отсутствовала, это не привело бы к компиляции и вместо этого привело бы к ошибке:

'when' expression must be exhaustive, add necessary 'else' branch

6. Завершение

Закрытые классы могут стать бесценным инструментом для нашего набора инструментов проектирования API. Использование хорошо известной структурированной иерархии классов, которая может быть только одним из ожидаемого набора классов, может помочь устранить целый набор потенциальных ошибок из нашего кода, в то же время упрощая чтение и обслуживание.

Оригинал статьи: https://www.baeldung.com/kotlin/sealed-classes