Вложенные и объединённые запечатанные иерархии
Создавайте сложные автоматы состояний, вкладывая запечатанные типы.
«Вложенные и объединённые запечатанные иерархии» — бесплатный урок Kotlin Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Kotlin Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Kotlin Academy содержит 4 уроков всего.
Построение деревьев состояний
В реальных приложениях состояние часто имеет несколько измерений. Вкладывайте запечатанные типы или объединяйте отдельные иерархии, чтобы аккуратно моделировать сложные предметные области.
Вложенный запечатанный класс
Объявляйте дочерние запечатанные типы внутри внешнего запечатанного типа. Компилятор по-прежнему проверяет полноту по всему дереву.
sealed class UiState {
object Loading : UiState()
sealed class Loaded : UiState() {
data class Success(val items: List<String>) : Loaded()
data class Empty(val message: String) : Loaded()
}
data class Error(val msg: String) : UiState()
}
fun render(s: UiState) = when (s) {
UiState.Loading -> "loading"
is UiState.Loaded.Success -> "${s.items.size} items"
is UiState.Loaded.Empty -> "empty: ${s.message}"
is UiState.Error -> "err: ${s.msg}"
}
fun main() {
println(render(UiState.Loaded.Success(listOf("a", "b"))))
}Запечатанные интерфейсы для примесей
Используйте запечатанные интерфейсы, когда состояние имеет несколько независимых аспектов — каждый аспект представлен отдельным интерфейсом.
sealed interface Authenticated
data class User(val id: Int) : Authenticated
object Guest
sealed interface Permission
object Read : Permission
object Write : Permission
fun describe(a: Authenticated, p: Permission): String = when {
a is User && p is Write -> "user ${a.id} can write"
a is User -> "user ${a.id} can read"
else -> "?"
}
fun main() {
println(describe(User(1), Write))
}Объединение иерархий
Передавайте два запечатанных значения и переключайтесь по их комбинации — используйте when с условиями.
sealed class Theme { object Light : Theme(); object Dark : Theme() }
sealed class Lang { object En : Lang(); object Tr : Lang() }
fun greet(theme: Theme, lang: Lang) = when {
theme is Theme.Dark && lang is Lang.Tr -> "Karanlik Merhaba"
theme is Theme.Light && lang is Lang.Tr -> "Aydinlik Merhaba"
theme is Theme.Dark -> "Dark Hello"
theme is Theme.Light -> "Light Hello"
else -> "?"
}
fun main() {
println(greet(Theme.Dark, Lang.Tr))
}Конечный автомат с вложенными вариантами
Моделируйте многоэтапные рабочие процессы как вложенные запечатанные типы — у каждого шага есть собственные варианты.
sealed class Checkout {
object Start : Checkout()
sealed class Payment : Checkout() {
object Pending : Payment()
data class Failed(val reason: String) : Payment()
object Captured : Payment()
}
object Shipped : Checkout()
object Delivered : Checkout()
}
fun describe(c: Checkout) = when (c) {
Checkout.Start -> "starting"
Checkout.Payment.Pending -> "awaiting payment"
is Checkout.Payment.Failed -> "payment failed: ${c.reason}"
Checkout.Payment.Captured -> "paid"
Checkout.Shipped -> "shipped"
Checkout.Delivered -> "delivered"
}
fun main() {
println(describe(Checkout.Payment.Failed("card declined")))
}Повторное использование запечатанных иерархий в разных модулях
Запечатанные типы лучше всего определять отдельно в каждом модуле. Для композиции между модулями предоставьте sealed interface, чтобы каждый модуль мог добавлять собственные реализации.
sealed interface Notification
data class Email(val to: String) : Notification
data class Sms(val phone: String) : Notification
fun send(n: Notification) = when (n) {
is Email -> "sending email to ${n.to}"
is Sms -> "texting ${n.phone}"
}
fun main() {
println(send(Email("a@example.com")))
}Общие свойства базового типа
Запечатанные классы могут объявлять общие свойства — все подтипы наследуют их. Это позволяет не дублировать состояние и метаданные, соблюдая принцип DRY.
sealed class HttpResponse(val code: Int) {
class Ok(val body: String) : HttpResponse(200)
class NotFound(val path: String) : HttpResponse(404)
class ServerError(val cause: String) : HttpResponse(500)
}
fun describe(r: HttpResponse) = "[${r.code}] " + when (r) {
is HttpResponse.Ok -> r.body
is HttpResponse.NotFound -> "not found: ${r.path}"
is HttpResponse.ServerError -> "fail: ${r.cause}"
}
fun main() {
println(describe(HttpResponse.Ok("hello")))
}Моделирование рекурсивных деревьев
Запечатанные типы естественным образом описывают рекурсивные структуры, например деревья выражений.
sealed class Expr {
data class Num(val value: Int) : Expr()
data class Add(val left: Expr, val right: Expr) : Expr()
data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
is Expr.Num -> e.value
is Expr.Add -> eval(e.left) + eval(e.right)
is Expr.Mul -> eval(e.left) * eval(e.right)
}
fun main() {
val expr = Expr.Add(Expr.Num(3), Expr.Mul(Expr.Num(4), Expr.Num(5)))
println(eval(expr)) // 23
}Сочетание с обобщениями
Запечатанные типы могут быть обобщёнными, что позволяет создавать типизированные оболочки результатов, например Result<T>.
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Failure(val message: String) : ApiResult<Nothing>()
}
fun handle(r: ApiResult<String>) = when (r) {
is ApiResult.Success -> "got ${r.data}"
is ApiResult.Failure -> "err: ${r.message}"
}
fun main() {
println(handle(ApiResult.Success("hi")))
println(handle(ApiResult.Failure("nope")))
}Антишаблон: слишком глубокая вложенность
Глубокая вложенность скрывает замысел. Если запечатанное дерево содержит более двух уровней, рассмотрите возможность его упрощения или вынесения поддеревьев в собственные файлы.
sealed class Top {
sealed class Mid : Top() {
sealed class Deep : Mid() {
object Leaf : Deep()
}
}
}
fun main() {
val l = Top.Mid.Deep.Leaf
println(l::class.simpleName)
}Объединение с помощью композиции
Если два измерения состояния независимы, предпочитайте композицию (класс данных, содержащий два запечатанных значения) глубоко вложенным иерархиям.
sealed class Loading { object Active : Loading(); object Idle : Loading() }
sealed class Auth { data class Signed(val id: Int) : Auth(); object Anon : Auth() }
data class AppState(val loading: Loading, val auth: Auth)
fun main() {
val s = AppState(Loading.Active, Auth.Signed(1))
println(s)
}Быстрая проверка
Какой подход рекомендуется, когда два измерения состояния независимы?
Итоги
Вкладывайте запечатанные типы для представления состояния в виде дерева; объединяйте независимые запечатанные иерархии с помощью композиции (обёртывающего класса данных). Добавляйте общие свойства в запечатанный базовый тип, используйте обобщения для типизированных результатов и естественно моделируйте рекурсивные структуры. Сохраняйте небольшую глубину вложенности.
Часто задаваемые вопросы
Урок «Вложенные и объединённые запечатанные иерархии» бесплатный?
Да — полный текст урока «Вложенные и объединённые запечатанные иерархии» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Kotlin Academy, подпишись на CoddyKit PRO. Курс Kotlin Academy содержит 4 уроков всего.
Чему я научусь в уроке «Вложенные и объединённые запечатанные иерархии»?
Создавайте сложные автоматы состояний, вкладывая запечатанные типы. Ты практикуешь Kotlin Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Kotlin Academy?
Предыдущий опыт не требуется. Kotlin Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Вложенные и объединённые запечатанные иерархии»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Kotlin Academy?
Да. Каждый урок Kotlin Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- sealed class и sealed interface: когда что использовать
- Полное when с запечатанными иерархиями
- Моделирование состояния интерфейса с помощью запечатанных классов
- Вложенные и объединённые запечатанные иерархии