Советы и трюки для программирования на языке 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.
После преобразования можно ознакомиться с представлением кода на 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")
}