Советы и трюки для программирования на языке Kotlin

0
11712

Полезные трюки, с которыми проще писать на языке 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 Bytecode

DecompileДекомпилируем байткод 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")
}

Другие материалы по теме:

РУБРИКИ В СТАТЬЕ

Комментарии 0

ВАКАНСИИ

Frontend разработчик (react native)
по итогам собеседования
Tableau developer
по итогам собеседования
Ведущий программист Unity3D
Москва, по итогам собеседования
Unreal Engine Developer
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG