Connect with us

Программирование

Элегантная обработка событий в Kotlin — рефакторинг в 7 шагов

Достигните пика производительности Kotlin-кода с помощью этих экспертных советов по рефакторингу.

Опубликовано

/

     
     

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

Чего мы хотим достичь

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

  • Замену запутанного оператора when на HashMap для молниеносной (O(1)) производительности.
  • Придание синтаксической легкости с помощью встроенных функций и реифицированных параметров типа.
  • Использование делегированных свойств для более чистого внедрения зависимостей.
  • Соблюдение принципа единой ответственности путем включения нескольких специализированных функций-обработчиков событий.

Шаг 1: Старт

Наше приключение начинается со взгляда на исходный код. Эта кодовая база управляет различными событиями блока с помощью функции handleBlockEvent и функции-обработчика события onEvent. Давайте раскроем исходный код:

open fun onEvent(event: Event) {    
    // ...
    handleBlockEvent(engine, getBlockForEvents(), checkNotNull(assetsRepo.fontFamilies.value).getOrThrow())
}

fun handleBlockEvent(engine: Engine, block: DesignBlock, fontFamilyMap: Map<String, FontFamilyData>, event: BlockEvent) {
    when (event) {
        BlockEvent.OnDelete -> engine.delete(block)
        BlockEvent.OnBackward -> engine.sendBackward(block)
        BlockEvent.OnDuplicate -> engine.duplicate(block)
        BlockEvent.OnForward -> engine.bringForward(block)
        BlockEvent.ToBack -> engine.sendToBack(block)
        BlockEvent.ToFront -> engine.bringToFront(block)
        BlockEvent.OnChangeFinish -> engine.editor.addUndoStep()
        is BlockEvent.OnChangeBlendMode -> onChangeBlendMode(engine, block, event.blendMode)
        is BlockEvent.OnChangeOpacity -> engine.block.setOpacity(block, event.opacity)
        is BlockEvent.OnChangeFillColor -> onChangeFillColor(engine, block, event.color)
        // and so on...
    }
}

sealed class BlockEvent : Event {
    object OnChangeFinish : BlockEvent
    object OnForward : BlockEvent
    object OnBackward : BlockEvent
    object OnDuplicate : BlockEvent
    object OnDelete : BlockEvent
    object ToFront : BlockEvent
    object ToBack : BlockEvent
    data class OnChangeBlendMode(val blendMode: BlendMode) : BlockEvent
    data class OnChangeOpacity(val opacity: Float) : BlockEvent
    data class OnChangeFillColor(val color: Color) : BlockEvent
    // and so on...
}

Чтобы использовать такой код, вы обычно вызываете функцию onEvent с определенным событием:

onEvent(BlockEvent.OnChangeFillColor(Color.RED))

Это вызовет функцию handleBlockEvent для обработки события. Итак, давайте начнем наше первое рефакторинговое приключение.

Шаг 2: HashMap и полезная нагрузка для максимальной производительности

В нашем первом акте рефакторинга мы используем надежный HashMap для сопоставления каждого типа события с соответствующим действием. Этот героический шаг устраняет необходимость в запутанном операторе when, делая наш код более эффективным. Мы также открываем механизм полезной нагрузки (payload) для передачи важных данных обработчикам событий.

Посмотрите на переделанный код:

abstract class EventsHandler<Payloads>(
    val fillPayload: (cache: Payloads) -> Unit
) {
    abstract val payloadCache: Payloads
    private val eventMap = mutableMapOf<KClass<out Event>, Payloads.(event: Event) -> Unit>()

    fun handleEvent(event: Event) {
        eventMap[event::class]?.let {
            it.invoke(payloadCache.also { fillPayload(it) }, event)
        }
    }

    operator fun <EventType : Event> set(event: KClass<out EventType>, lambda: Payloads.(event: EventType) -> Unit) {
        eventMap[event] = lambda as Payloads.(event: Event) -> Unit
    }
}

class BlockEventsHandler(fillPayload: (cache: BlockEventsHandler.Payloads) -> Unit) : EventsHandler<BlockEventsHandler.Payloads>(fillPayload) {
    class Payloads {
        lateinit var engine: Engine
        lateinit var block: DesignBlock
        lateinit var fontFamilyMap: Map<String, FontFamilyData>
    }
    override val payloadCache: Payloads = Payloads()

    init {
        it[BlockEvent.OnDelete::class] = { engine.delete(block) }
        it[BlockEvent.OnBackward::class] = { engine.sendBackward(block) }
        it[BlockEvent.OnDuplicate::class] = { engine.duplicate(block) }
        it[BlockEvent.OnForward::class] = { engine.bringForward(block) }
        it[BlockEvent.ToBack::class] = { engine.sendToBack(block) }
        it[BlockEvent.ToFront::class] = { engine.bringToFront(block) }
        it[BlockEvent.OnChangeFinish::class] = { engine.editor.addUndoStep() }
        it[BlockEvent.OnChangeBlendMode::class] = { onChangeBlendMode(engine, block, it.blendMode) }
        it[BlockEvent.OnChangeOpacity::class] = { engine.block.setOpacity(block, it.opacity) }
        it[BlockEvent.OnChangeFillColor::class] = { onChangeFillColor(engine, block, it.color) }
        // and so on...
    }
}

private val blockEventHandler = BlockEventsHandler {
    it.engine = engine
    it.block = getBlockForEvents()
    it.fontFamilyMap = checkNotNull(assetsRepo.fontFamilies.value).getOrThrow()
}

open fun onEvent(event: Event) {    
    // ...
    blockEventHandler.handleEvent(event)
}

Повышение производительности

Используя возможности HashMap, мы увеличили производительность обработки событий. Временная сложность обработки события теперь составляет молниеносную величину (O(1)), что является монументальным улучшением по сравнению с временной сложностью (O(n)) громоздкого оператора when. В тоже время наш механизм полезной нагрузки добавляет синтаксический сахар. Он позволяет нам объединить все необходимые данные в один объект, что делает наш код более понятным и удобным.

Примечание: Использование HashMap вместо большого оператора when() дает значительный прирост производительности. Оно может быть в 40-150 раз быстрее. Однако объяснение деталей вышло бы за рамки этой статьи. Поэтому я расскажу о нем, а также о других головоломках производительности Kotlin, в одной из следующих статей.

Пока же рефакторинговый код остается таким же простым, как и раньше:

onEvent(BlockEvent.OnChangeFillColor(Color.RED))

По-прежнему вызывается метод handleEvent в BlockEventsHandler, который, в свою очередь, выполняет соответствующее действие в зависимости от типа события. Сам BlockEvent — это объект данных, содержащий все детали события, и он служит параметром лямбды.

Замечание о полезной нагрузке

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

Шаг 3: Добавление синтаксического сахара с помощью Infix функций

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

Посмотрите на обновленный код:

abstract class EventsHandler<Payloads>(
    val fillPayload: (cache: Payloads) -> Unit
) {
    infix fun <Payloads, EventType : Event> KClass<out EventType>.to(lambda: Payloads.(event: EventType) -> Unit) {
        eventMap[event] = lambda as Payloads.(event: Event) -> Unit
    }
    // ... (rest of the code remains the same)
}

class BlockEventsHandler(
    manager: EventsManager,
    override val fillPayload: (cache: TextBlockEventsHandler) -> Unit
) : EventsHandler<TextBlockEventsHandler>(manager) {
    lateinit var engine: Engine
    lateinit var block: DesignBlock
    lateinit var fontFamilyMap: Map<String, FontFamilyData>

    init {
        BlockEvent.OnDelete::class to { 
            engine.delete(block) 
        }
        BlockEvent.OnBackward::class to { 
            engine.sendBackward(block) 
        }
        BlockEvent.OnDuplicate::class to { 
            engine.duplicate(block) 
        }
        BlockEvent.OnForward::class to { 
            engine.bringForward(block) 
        }
        BlockEvent.ToBack::class to { 
            engine.sendToBack(block) 
        }
        BlockEvent.ToFront::class to { 
            engine.bringToFront(block) 
        }
        BlockEvent.OnChangeFinish::class to { 
            engine.editor.addUndoStep() 
        }
        BlockEvent.OnChangeBlendMode::class to { 
            onChangeBlendMode(engine, block, it.blendMode) 
        }
        BlockEvent.OnChangeOpacity::class to { 
            engine.block.setOpacity(block, it.opacity) 
        }
        BlockEvent.OnChangeFillColor::class to { 
            onChangeFillColor(engine, block, it.color) 
        }
        // ...
    }
}

Синтаксический сахар и производительность

Введение инфиксной функции to добавляет синтаксическую сладость, которая улучшает выразительность кода и позволяет использовать его более естественно. Благодаря этому становится предельно ясно, для чего нужно каждое событие. И не бойтесь, производительность остается на уровне (O(1)), благодаря нашему надежному HashMap.

Гибкость в синтаксисе

Хотя здесь используется ключевое слово to, не стесняйтесь заменять его другими терминами, такими как handle, trigger или любыми другими, наиболее подходящими для вашего контекста. Гибкость — это главное.

Шаг 4: Использование Inline функций для элегантности

Однако это все равно не идеально, потому что класс ::class нарушает чтение.

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

Это станет возможным благодаря встроенной функции с проверенным параметром типа, которая сохраняет ссылку на класс во время выполнения.

Для этого мы расширяем класс EventsHandler новой функцией register:

class EventsHandler(
    register: EventsHandler.() -> Unit,
) {
    inline fun <reified EventType : BaseEvent> register(noinline lambda: (event: EventType) -> Unit) : Any {
        this[EventType::class] = lambda
        return lambda
    }
   // ... (rest of the code remains the same)
}

Новый синтаксис

Вот как выглядит регистрация обработчика событий в новом синтаксисе:

register<BlockEvent.OnChangeLineWidth> {
    engine.block.setWidth(block, engine.block.getFrameWidth(block))
    engine.block.setHeight(block, it.width)
}

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

Шаг 5: Повышение register до функции расширения

Чтобы улучшить читаемость кода, мы сделаем тонкий, но эффективный шаг, преобразовав функцию register из функции класса EventsHandler в функцию расширения EventsHandler.

Звучит глупо! Так зачем?

Это небольшое изменение улучшает читаемость кода, выделяя ключевое слово register с помощью подсветки синтаксиса из функции расширения Kotlin. Это сделает код более красочным, что улучшит читабельность.

Обновленный класс EventsHandler

Класс EventsHandler остался практически без изменений, но функция register теперь находится за пределами класса и преобразована в функцию расширения для класса EventsHandler:

class EventsHandler(
    register: EventsHandler.() -> Unit,
) {
    // ... (rest of the code remains the same)
}

inline fun <reified EventType : BaseEvent> EventsHandler.register(noinline lambda: (event: EventType) -> Unit) : Any {
    this[EventType::class] = lambda
    return lambda
}

За счет того, что мы вынесли register за пределы класса, само определение класса EventsHandler теперь выделяется характерной подсветкой синтаксиса. Это умный трюк, который не влияет на производительность во время выполнения или компиляции, так как это в любом случае инлайн-операция.

**register**<BlockEvent.OnChangeLineWidth> {
    engine.block.setWidth(block, engine.block.getFrameWidth(block))
    engine.block.setHeight(block, it.width)
}

Шаг 6: Устранение lateinit переменных с помощью делегированных свойств

Пришло время разобраться с загадочными lateinit переменными и несколько запутанным механизмом fillPayload. Давайте представим более чистый подход, используя делегируемые свойства и лямбда-функции для инъекции зависимостей.

Добавим класс Inject, чтобы обернуть обычную лямбду как делегируемую:

class Inject<Type>(private val inject: () -> Type) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Type = inject()
}

С этой новообретенной силой наш код обработчика событий стал чище и интуитивно понятнее. Он приобрел стиль декларативного синтаксиса Jetpack Compose:

fun EventsHandler.textBlockEvents(
    engine: () -> Engine,
    block: () -> DesignBlock,
    fontFamilyMap: () -> Map<String, FontFamilyData>,
) {
    // Inject the dependencies
    val engine by Inject(engine)
    val block by Inject(block)
    val fontFamilyMap by Inject(fontFamilyMap)

    // Event handling logic here
    // ...
}

Всякий раз, когда происходит обращение к одной из переменных, вызывается лямбда, и вы всегда получаете текущую переменную.

Кроме того, создание «полезной нагрузки» становится более простым, чистым и безопасным для типов. Это похоже на передачу переменной:

private val eventHandler = EventsHandler {
    textBlockEvents (
        engine = ::engine,
        block = ::getBlockForEvents,
        fontFamilyMap = { checkNotNull(assetsRepo.fontFamilies.value).getOrThrow() },
    )
}

Выглядит и ощущается как волшебство! Очень круто, правда?

Шаг 7: Несколько обработчиков событий для принципа единой ответственности

В финале мы используем новую гибкость, полученную в результате предыдущих изменений, для регистрации нескольких функций обработчиков событий. Каждая функция регистрации в обработчике событий теперь имеет определенную тему, что прекрасно согласуется с принципом единой ответственности (SRP).

Расширенная регистрация обработчиков событий

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

private val eventHandler = EventsHandler {
    cropEvents(
        engine = ::engine,
        block = ::getBlockForEvents,
    )
    blockEvents (
        engine = ::engine,
        block = ::getBlockForEvents,
    )
    textBlockEvents (
        engine = ::engine,
        block = ::getBlockForEvents,
        fontFamilyMap = { checkNotNull(assetsRepo.fontFamilies.value).getOrThrow() },
    )
    // ...
}

fun EventsHandler.blockEvents(
    engine: () -> Engine,
    block: () -> DesignBlock
) {
    val engine: Engine by Inject(engine)
    val block: DesignBlock by Inject(block)

    register<BlockEvent.OnDelete> { engine.delete(block) }

    register<BlockEvent.OnBackward> { engine.sendBackward(block) }

    register<BlockEvent.OnDuplicate> { engine.duplicate(block) }

    register<BlockEvent.OnForward> { engine.bringForward(block) }

    register<BlockEvent.ToBack> { engine.sendToBack(block) }

    register<BlockEvent.ToFront> { engine.bringToFront(block) }

    register<BlockEvent.OnChangeFinish> { engine.editor.addUndoStep() }

    register<BlockEvent.OnChangeBlendMode> {
        if (engine.block.getBlendMode(block) != it.blendMode) {
            engine.block.setBlendMode(block, it.blendMode)
            engine.editor.addUndoStep()
        }
    }

    register<BlockEvent.OnChangeOpacity> { engine.block.setOpacity(block, it.opacity) }
}

fun EventsHandler.cropEvents(
    engine: () -> Engine,
    block: () -> DesignBlock
) {
    val engine: Engine by Inject(engine)
    val block: DesignBlock by Inject(block)
    // ... (event handling logic for cropping events)
}

fun EventsHandler.textBlockEvents(
    engine: () -> Engine,
    block: () -> DesignBlock,
    fontFamilyMap: () -> Map<String, FontFamilyData>,
) {
    val engine by Inject(engine)
    val block by Inject(block)
    val fontFamilyMap by Inject(fontFamilyMap)
    // ... (event handling logic for text block events)
}

При этом обращение к нему и его API остаются неизменными, и никаких дополнительных параметров передавать не нужно:

open fun onEvent(event: Event) {
    eventHandler.handleEvent(event)
}

Заключение

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

Если вы хотите попробовать, я создал рабочий пример кода в Kotlin Playground.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Хорошие новости

Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: