Советы и трюки для программирования на языке Kotlin
Полезные трюки, с которыми проще писать на языке Kotlin. В этой статье рассмотрим изолированные классы и функцию when() для перестановок.
Изолированные классы в языке Kotlin
Изолированный класс – не новое понятие для программирования: это довольно известная концепция. Такие классы используются для отражения ограниченных иерархий классов, когда значение может иметь тип только из ограниченного набора.
Изолированные классы представляют собой расширение enum-классов: набор значений enum-типа также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник изолированного класса способен иметь множество экземпляров, которые могут нести в себе какое-то состояние.
Kotlin предоставляет специальное ключевое слово sealed для создания закрытых классов:
sealed class Response data class Success(val body: String): Response() data class Error(val code: Int, val message: String): Response() object Timeout: Response()
На первый взгляд может показаться, что этот код ничего не делает, кроме объявления sealed-класса. Чтобы глубже разобраться с тем, что происходит, воспользуемся инструментом IntelliJ IDEA Kotlin Bytecode.
Найдем Kotlin Bytecode
Декомпилируем байткод Kotlin в Java
После преобразования можно ознакомиться с представлением кода на Java:
public abstract class Response { private Response() { } // $FF: synthetic method public Response(DefaultConstructorMarker $constructor_marker) { this(); } }
Изолированные классы созданы специально для наследования. Кроме того, компилятор Kotlin позволяет использовать подклассы класса Response в качестве кейсов функции when(). Структуры, наследуемые из изолированного класса, могут быть объявлены как данные и даже как объект.
fun sugar(response: Response) = when (response) { is Success -> ... is Error -> ... Timeout -> ... }
Экземпляр Response можно использовать без каких-либо дополнительных преобразований:
fun sugar(response: Response) = when (response) { is Success -> println(response.body) is Error -> println("${response.code} ${response.message}") Timeout -> println(response.javaClass.simpleName) }
Чтобы напомнить себе, как выглядел бы код без sealed-классов и Kotlin в целом, давайте еще раз воспользуемся IntelliJ IDEA Kotlin Bytecode:
public final void sugar(@NotNull Response response) { Intrinsics.checkParameterIsNotNull(response, "response"); String var3; if (response instanceof Success) { var3 = ((Success)response).getBody(); System.out.println(var3); } else if (response instanceof Error) { var3 = "" + ((Error)response).getCode() + ' ' + ((Error)response).getMessage(); System.out.println(var3); } else { if (!Intrinsics.areEqual(response, Timeout.INSTANCE)) { throw new NoWhenBranchMatchedException(); } var3 = response.getClass().getSimpleName(); System.out.println(var3); } }
Использование when() для перестановок
Мы уже сталкивались с возможностями функции when() выше. Давайте теперь представим, что необходимо реализовать функцию, которая принимает два перечисления и создает неизменяемое состояние.
enum class Employee { DEV_LEAD, SENIOR_ENGINEER, REGULAR_ENGINEER, JUNIOR_ENGINEER } enum class Contract { PROBATION, PERMANENT, CONTRACTOR, }
Enum-класс Employee описывает все роли, которые могут быть найдены в компании XYZ, а enum-класс Contract содержит все виды трудового договора. На основе двух этих перечислений необходимо вернуть правильный SafariBookAccess. Кроме того, функция должна создавать все состояния для данных перечислений. В качестве первого шага создадим прототип подписи функции, создающей состояние:
fun access(employee: Employee, contract: Contract): SafariBookAccess
Теперь определим структуру SafariBookAccess, используя волшебное sealed:
sealed class SafariBookAccess data class Granted(val expirationDate: DateTime) : SafariBookAccess() data class NotGranted(val error: AssertionError) : SafariBookAccess() data class Blocked(val message: String) : SafariBookAccess()
Осталось заняться основной задачей функции access() – перестановкой:
fun access(employee: Employee, contract: Contract): SafariBookAccess { return when (employee) { SENIOR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Granted(DateTime()) CONTRACTOR -> Granted(DateTime()) } REGULAR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Granted(DateTime()) CONTRACTOR -> Blocked("Access blocked for $contract.") } JUNIOR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Blocked("Access blocked for $contract.") CONTRACTOR -> Blocked("Access blocked for $contract.") } else -> throw AssertionError() } }
Выглядит здорово, но можно ли сделать так, чтобы код выглядел более по котлински? Вот на что стоит обратить внимание:
- Слишком много when(). Лучше использовать Pair вместо вложенности.
- Изменить порядок перечислений. Пары определить как Pair<Contract, чтобы сделать Employee>() более читабельным.
- Объединить повторения в return.
fun access(contract: Contract, employee: Employee) = when (Pair(contract, employee)) { Pair(PROBATION, SENIOR_ENGINEER), Pair(PROBATION, REGULAR_ENGINEER), Pair(PROBATION, JUNIOR_ENGINEER) -> NotGranted(AssertionError("Access not allowed on probation contract.")) Pair(PERMANENT, SENIOR_ENGINEER), Pair(PERMANENT, REGULAR_ENGINEER), Pair(PERMANENT, JUNIOR_ENGINEER), Pair(CONTRACTOR, SENIOR_ENGINEER) -> Granted(DateTime(1)) Pair(CONTRACTOR, REGULAR_ENGINEER), Pair(CONTRACTOR, JUNIOR_ENGINEER) -> Blocked("Access for junior contractors is blocked.") else -> throw AssertionError("Unsupported case of $employee and $contract") }
Теперь функция выглядит более чистой и короткой, но в языке Kotlin есть еще одна приятная возможность – полностью избежать объявления Pair:
fun access(contract: Contract, employee: Employee) = when (contract to employee) { PROBATION to SENIOR_ENGINEER, PROBATION to REGULAR_ENGINEER -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT to SENIOR_ENGINEER, PERMANENT to REGULAR_ENGINEER, PERMANENT to JUNIOR_ENGINEER, CONTRACTOR to SENIOR_ENGINEER -> Granted(DateTime(1)) CONTRACTOR to REGULAR_ENGINEER, PROBATION to JUNIOR_ENGINEER, CONTRACTOR to JUNIOR_ENGINEER -> Blocked("Access for junior contractors is blocked.") else -> throw AssertionError("Unsupported case of $employee and $contract") }