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

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

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