- PVSM.RU - https://www.pvsm.ru -
Наверное, не я один после прочтения документации о sealed классах [1] подумал: «Ладно. Может быть это когда-нибудь пригодится». Позже, когда в работе я встретил пару задач, где удалось успешно применить этот инструмент, я подумал: «Недурно. Стоит чаще задумываться о применении». И, наконец, я наткнулся на описание класса задач в книге Effective Java (Joshua Bloch, 3rd) [2] (да-да, в книге о Java).
Давайте рассмотрим один из вариантов применения и оценим его с точки зрения семантики и производительности.
Думаю, все, кто работал с UI, когда-то встречали реализации взаимодействия UI с сервисом через некие состояния, где одним из атрибутов был какой-то маркер типа. Механика обработки очередного состояния в таких реализациях, обычно, напрямую зависит от указанного маркера. Например такая реализация класса State:
class State(
val type: Type,
val data: String?,
val error: Throwable?
) {
enum class Type { LOADING, ERROR, EMPTY, DATA }
}
Замечания из главы 23 «Prefer class hierarchies to tagged classes» книги. Предлагаю ознакомиться и с ней.
Обработка нового состояния может выглядеть так:
fun handleState(state: State) {
when(state.type) {
State.Type.LOADING -> onLoading()
State.Type.ERROR -> state.error?.run(::onError)
?: throw AssertionError("Unexpected error state: $state")
State.Type.EMPTY -> onEmpty()
State.Type.DATA -> state.data?.run(::onData)
?: throw AssertionError("Unexpected data state: $state")
}
}
fun onLoading() {}
fun onError(error: Throwable) {}
fun onEmpty() {}
fun onData(data: String) {}
Обратите внимание, для состояний типа ERROR и DATA компилятор не в состоянии определить безопасность использования атрибутов, поэтому пользователю приходятся писать избыточный код. Изменения в семантике можно будет выявить только во время исполнения.
Несложным рефакторингом, мы можем разбить наш State на группу классов:
sealed class State
// Состояние загрузки является stateless объектом - можно оформить в виде singleton
object Loading : State()
data class Error(val error: Throwable) : State()
// Отсутствие данных, равно как и состояние загрузки, является stateless объектом - тоже singleton
object Empty : State()
data class Data(val data: String) : State()
На стороне пользователя, мы получим обработку состояний, где доступность атрибутов будет определяться на уровне языка, а неверное использование будет порождать ошибки на этапе компиляции:
fun handleState(state: State) {
when(state) {
Loading -> onLoading()
is Error -> onError(state.error)
Empty -> onEmpty()
is Data -> onData(state.data)
}
}
Так как в экземплярах присутствуют только значимые атрибуты можно говорить об экономии памяти и, что немаловажно, об улучшении семантики. Пользователям sealed классов не нужно вручную реализовывать правила работы с атрибутами в зависимости от маркера типа, доступность атрибутов обеспечивается разделением на типы.
Java разработчики, кто пробовал Kotlin, наверняка заглядывали в декомпилированный код, чтобы посмотреть, на что похожи Kotlin выражения в терминах Java. Выражение с when будет выглядеть примерно так:
public static final void handleState(@NotNull State state) {
Intrinsics.checkParameterIsNotNull(state, "state");
if (Intrinsics.areEqual(state, Loading.INSTANCE)) {
onLoading();
} else if (state instanceof Error) {
onError(((Error)state).getError());
} else if (Intrinsics.areEqual(state, Empty.INSTANCE)) {
onEmpty();
} else if (state instanceof Data) {
onData(((Data)state).getData());
}
}
Ветвления с изобилием instanceof могут насторожить из-за стереотипов о «признаке плохого кода» и «влиянии на производительность», но нам ни к чему догадки. Нужно каким-то образом сравнить скорость выполнения, например, с помощью jmh [3].
На основе статьи «Измеряем скорость кода Java правильно» [4] был подготовлен тест [5] обработки четырёх состояний (LOADING, ERROR, EMPTY, DATA), вот его результаты:
Benchmark Mode Cnt Score Error Units
CompareSealedVsTagged.sealed thrpt 500 940739,966 ± 5350,341 ops/s
CompareSealedVsTagged.tagged thrpt 500 1281274,381 ± 10675,956 ops/s
Видно, что sealed реализация работает ≈25% медленнее (было предположение, что отставание не превысит 10-15%).
Если на четырёх типах мы имеем отставание на четверть, с увеличением типов (количество проверок instanceof) отставание должно только расти. Для проверки увеличим количество типов до 16 (предположим, что нас угораздило обзавестись настолько широкой иерархией):
Benchmark Mode Cnt Score Error Units
CompareSealedVsTagged.sealed thrpt 500 149493,062 ± 622,313 ops/s
CompareSealedVsTagged.tagged thrpt 500 235024,737 ± 3372,754 ops/s
Вместе со снижением производительности возросло отставание sealed реализации до ≈35% — чуда не произошло.
В этой статье мы не открыли Америку и sealed реализации в ветвлениях на instanceof действительно работают медленнее сравнения ссылок.
Тем не менее нужно озвучить пару мыслей:
Автор: XIII-th
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/341209
Ссылки в тексте:
[1] документации о sealed классах: https://kotlinlang.org/docs/reference/sealed-classes.html
[2] Effective Java (Joshua Bloch, 3rd): https://www.amazon.com/Effective-Java-3rd-Joshua-Bloch/dp/0134685997
[3] jmh: https://openjdk.java.net/projects/code-tools/jmh/
[4] «Измеряем скорость кода Java правильно»: https://habr.com/ru/post/349914/
[5] тест: https://github.com/XIII-th/sealed-performance-test
[6] Источник: https://habr.com/ru/post/430014/?utm_campaign=430014&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.