Рубрики
Без рубрики

Игра с закрытыми классами Kotlin

Во время работы над моей магистерской диссертацией мне недавно пришлось настраивать вероятности. Эти значения были одинаковыми… Помеченный kotlin, jackson, java.

Во время работы над моей магистерской диссертацией мне недавно пришлось настраивать вероятности. Эти значения были организованы в древовидную структуру. Из-за этого я подумал об использовании наследования – и с уже существующим Kotlin использование закрытых классов показалось мне, по крайней мере, интересной возможностью.

Отказ от ответственности: Этот пост просто для развлечения и для демонстрации некоторых интересных вещей, специфичных для Kotlin. В конце концов, использование Jackson для десериализации входных данных в классы данных, вероятно, более эффективно.

Образец

Допустим, мы хотим настроить вероятность в процентах для некоторых действий, например, используя YAML. Это может выглядеть следующим образом:

Configuration:
    Greeting:
        SayHello: 70
        WaveAt: 80
        Hug: 5
    Talking:
        DoSmallTalk: 90
        Insult: 1

Как вы можете видеть, здесь есть два подраздела. Однако при этом можно легко получить произвольную глубину.

Прежде чем мы воспроизведем эту структуру, мы должны уточнить, что такое закрытые классы.

Закрытые Классы

В официальной документации они называются расширениями классов enum, используемых для представления ограниченных иерархий классов.

Закрытые классы объявляются с использованием ключевого слова sealed , являются абстрактными и должны иметь только частные конструкторы.

Проще говоря, закрытый класс гарантирует, что все возможные подтипы известны во время компиляции. Это особенно удобно, когда используется в сочетании с предложением Kotlin when :

sealed class TruthOrDare
class Truth(val question: String) : TruthOrDare()
class Dare(val task: String) : TruthOrDare()

fun nextTurn(input: TruthOrDare) = when(input) {
    is Truth -> println("Answer the following question: ${input.question}")
    is Dare -> println("Your task is: ${input.task}")
    // the `else` clause is not required because we've covered all the cases
}

Запечатанные классы в действии

Давайте вернемся к исходной задаче: использование закрытых классов для синтаксического анализа конфигурации.

Структура приведенного выше фрагмента кода YAML может быть представлена следующим образом:

sealed class Configuration {

    sealed class Greeting : Configuration() {

        object SayHello : Greeting()
        object WaveAt : Greeting()
        object Hug : Greeting()

    }

    sealed class Talking : Configuration() {

        object DoSmallTalk : Talking()
        object Insult : Talking()

    }

}

У нас есть вложенные запечатанные подклассы и object s (сокращение для Singletons в Kotlin). Позже мы увидим, почему мы здесь используем объекты.

Однако пока там ничего не настроено. Итак, как мы можем использовать эту структуру?

Ответ таков: Отражение .

Отступление

Однако, прежде чем мы перейдем к фактической структуре, я хотел бы представить небольшой класс-оболочку, чтобы инкапсулировать наши выводы:

class Probabilities private constructor(
        private val backingMap: MutableMap = mutableMapOf()
) : Map by backingMap {

    constructor(configuration: Probabilities.() -> Unit) : this() {
        configuration()
    }

    infix fun Configuration.withProbabilityOf(percent: Int) = backingMap.put(this, percent)

    override fun toString(): String = backingMap.entries.joinToString { "${it.key::class.simpleName} = ${it.value}" }
}

В этом классе вы можете увидеть некоторые другие интересные функции Kotlin: реализация путем делегирования , функции приемника и инфиксные функции .

Реализация интерфейса Map может быть проблемой в Java. Kotlin предоставляет интеллектуальное решение с реализацией путем делегирования . Все, что вам нужно сделать, это использовать ключевое слово by в сочетании со значением, которое уже реализует интерфейс. Так что в этом случае мы просто делегируем все Сопоставить -конкретные операции с/| Изменяемая карта , который создается конструктором по умолчанию.

Чтобы упростить настройку класса Probabilities , мы предоставляем второй конструктор, принимающий функцию receiver . Это означает, что ключевое слово this предоставленного лямбда-выражения указывает на экземпляр класса Probabilities .

Другая функция приемника также обеспечивается с вероятностью . Однако это также функция infix , которая помечена ключевым словом infix и позволяет вам написать что-то вроде:

SayHello withProbabilityOf 99

Разбор

Наконец, вот код для синтаксического анализа, например, Map of String и Любой :

fun fromMap(configuration: Map) = Probabilities { // (1)
    @Suppress("UNCHECKED_CAST")
    fun parseFor( // (2)
            configuration: Map, // (3)
            parent: KClass?,
            clazz: KClass
    ) {
        // (4)
        if (configuration.containsKey(clazz.simpleName) && parent?.isSuperclassOf(clazz) != false) {
            when (val value = configuration[clazz.simpleName]) {
                is Map<*, *> -> { // (5)
                    clazz.sealedSubclasses.forEach { subclass ->
                        parseFor(value as Map, clazz, subclass)
                    }
                }
                is Int -> { // (6)
                    if (clazz.objectInstance == null)
                        throw RuntimeException("${clazz.simpleName} should be an object")

                    clazz.objectInstance!! withProbabilityOf value
                }
                else -> throw RuntimeException("unknown property ${clazz.simpleName}")
            }
        }
    }

    parseFor(configuration, null, Configuration::class)
}

Некоторое объяснение (обратите внимание на комментарии в коде):

  • (1) мы создаем новый экземпляр Probabilities , используя одну из величайших функций Kotlin: если последний параметр любой функции или конструктора является лямбда-функцией, мы можем опустить круглые скобки и использовать только фигурные скобки.
  • (2) еще одна прекрасная вещь – это локальные функции : поскольку нам не нужна рекурсивная функция синтаксический анализ Для в любом другом месте, мы просто объявляем его внутри нашего с карты функция
  • ((3) параметры нашей функции parseFor являются:

    • поддерево конфигурации
    • предполагаемый родительский элемент, как указано в конфигурации
    • текущий класс, указанный в свойстве конфигурации
  • (4) если где-то на верхнем уровне текущего поддерева найден ожидаемый класс и предполагаемый родительский класс соответствует реальному суперклассу, то мы используем when для типа значения свойства
  • (5) если это Карта , примените синтаксический анализ Для на каждом запечатанном подклассе
  • (6) если это Int и clazz – это объект , мы настраиваем вероятность на это значение

В приведенном выше коде вы можете увидеть, почему наличие оставляет быть object s – хорошая идея: всегда будет ровно один экземпляр. Таким образом, они идеально подходят для использования в качестве ключа на карте.

Сладкий. – Но почему?

Честно говоря, все вышесказанное может показаться чрезмерно продуманной конструкцией. По сравнению с примером, использующим закрытые классы, эквивалентная структура с классами данных была бы аналогичной и могла бы быть легко использована, например, с Джексоном из коробки:

data class Configuration(
    val greeting: Greeting?,
    val talking: Talking?
) {

    data class Greeting(
        val sayHello: Int?,
        val waveAt: Int?,
        val hug: Int?
    )

    data class Talking(
        val doSmallTalk: Int?,
        val insult: Int?
    )
}

Это самое замечательное в Kotlin: у вас есть так много языковых конструкций, которые помогают вам создавать свое программное обеспечение наилучшим способом, что бы это ни значило. Поэтому необходимо поиграть с этими возможностями и познакомиться с ними поближе.

Использование Джексона

Чтобы использовать концепцию использования закрытых классов в сочетании с Jackson, нам необходимо предоставить пользовательский десериализатор. Однако, поскольку у нас уже есть логика сопоставления, это простая задача:

class ProbabilitiesDeserializer : StdDeserializer(Probabilities::class.java) {

    override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?) =
            p?.readValueAs>(object : TypeReference>() {})
                    ?.let { fromMap(it) }

}

Для этого и возможности синтаксического анализа YAML нам понадобятся следующие зависимости:

  • com.fasterxml.jackson.module:джексон-модуль-котлин:2.11.2
  • com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.2

Наконец, мы готовы создать ObjectMapper и прочитать нашу конфигурацию YAML:

val mapper = ObjectMapper(YAMLFactory()).apply {
    registerModule(SimpleModule().addDeserializer(Probabilities::class.java, ProbabilitiesDeserializer()))
    registerModule(KotlinModule())
}
val probabilities = mapper.readValue(yamlInput)
println(probabilities.toString())
println("Say hello with probability of ${probabilities[SayHello]} percent.")

Результатом будет:

SayHello = 70, WaveAt = 80, Hug = 5, DoSmallTalk = 90, Insult = 1
Say hello with probability of 70 percent.

Это оно.

Бонус: Обеспечение всех свойств устанавливаются

Последнее, что я хотел добавить, – это то, как вы применяете все object s, которые необходимо настроить.

Все, что нам нужно сделать, это найти все листья нашего дерева конфигурации и сравнить их с содержимым входных данных:

val leaves = leavesOf(Configuration::class)
fun leavesOf(baseClass: KClass): List> =
    if (!baseClass.isSealed) {
        listOf(baseClass)
    } else {
        baseClass.sealedSubclasses.flatMap(::leavesOf)
    }

fun Probabilities.ensureAllActionsCovered() {
    val keys = keys.map { it::class }
    val unconfigured = leaves.filter { leaf -> !keys.contains(leaf) }
    if (unconfigured.isNotEmpty())
        throw RuntimeException("Unconfigured leaves: ${unconfigured.joinToString()}")
}

Наконец, мы увидели последнюю классную функцию Kotlin: функции расширения . Они позволяют нам добавлять некоторую функциональность в закрытый класс, в противном случае. В каком-то смысле это как бонус.

Вы вызываете их так, как если бы они были методом класса:

probabilities.ensureAllActionsCovered()

Заключительные замечания

Спасибо за чтение, надеюсь, вам понравилось. Вы можете найти весь код в script на Github как суть .

Оригинал: “https://dev.to/alxgrk/playing-around-with-kotlin-sealed-classes-4kdh”