专栏名称: 稀土掘金技术社区
掘金,一个帮助开发者成长的技术社区
目录
相关文章推荐
北京吃货小分队  ·  “打车25公里,就为吃这一碗!” ·  3 天前  
北京亦庄  ·  北京市发布大风蓝色预警,请注意防范 ·  昨天  
北京本地宝  ·  北京将新增两座大型综合交通枢纽! ·  4 天前  
北京亦庄  ·  北京发布大风蓝色预警,阵风可达7级左右 ·  3 天前  
51好读  ›  专栏  ›  稀土掘金技术社区

优雅的Kotlin事件处理 - 重构

稀土掘金技术社区  · 公众号  ·  · 2024-01-23 15:48

正文

通过这些专业的重构技巧, 释放 Kotlin 代码的峰值性能.

在软件开发的世界里, 代码重构是将我们从纠结而低效的代码中解救出来的英雄. 在本文中, 我们将开始一场冒险, 用以重构处理各种事件的 Kotlin 代码. 我们的任务是什么? 提高性能和风格, 使代码更流畅, 更易维护, 并让我们的工作充满乐趣.

我们的目标

在改造 Kotlin 事件处理的过程中, 我们的目标是完善代码, 使其更高效, 更易读, 更易维护. 我们将引入各种改进, 包括:

  • HashMap代替复杂的when语句, 以获得快如闪电的(O(1))性能.
  • 使用内联函数和reified类型参数来优化语法.
  • 采用委托属性, 实现更简洁的依赖注入.
  • 通过启用多个专门的事件处理函数, 坚持单一责任原则.

第 1 步: 起跑线

我们的冒险之旅从查看原始代码开始. 该代码库通过名为handleBlockEvent的函数和名为onEvent的事件处理函数来管理各种块事件. 让我们揭开原始代码的神秘面纱:

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

fun handleBlockEvent(engine: Engine, block: DesignBlock, fontFamilyMap: Map, 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和Payload的神秘面纱, 实现峰值性能

在第一次重构中, 我们引入了值得信赖的HashMap来将每个事件类型映射到其对应的操作. 这一英勇之举消除了对复杂的when语句的需求, 使我们的代码更加高效. 我们还公布了一种Payload机制, 用于向事件处理程序传递重要数据.

请看重构后的代码:

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

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

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

class BlockEventsHandler(fillPayload: (cache: BlockEventsHandler.Payloads) -> Unit) : EventsHandler(fillPayload) {
class Payloads {
lateinit var engine: Engine
lateinit var block: DesignBlock
lateinit var fontFamilyMap: Map
}
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)), 与繁琐的when语句的(O(n))时间复杂度相比, 这是一个巨大的进步. 我们的Payload机制增加了语法糖. 它使我们能够将所有必要的数据捆绑到一个对象中, 从而使我们的代码更易读, 更易维护.

💡 注意: 使用HashMap代替大型when语句可显著提高性能. 它可以将速度提高 40 到 150 倍. 不过, 解释细节将超出本文的范围. 因此, 我将在今后的博文中介绍它以及其他 Kotlin 性能难题.

重构后的代码仍然和以前一样简单:

onEvent(BlockEvent.OnChangeFillColor(Color.RED))

这仍然会触发BlockEventsHandler中的handleEvent方法, 然后根据事件类型执行相应的操作. BlockEvent本身是一个包含所有事件细节的数据对象, 它可以作为 lambda 参数.

关于Payload的说明

Payload创建是一个动态 lambda 函数, 每次处理事件时都会执行. 这样可以确保所有不属于事件的变量都是最新的. 鉴于我们处理的是每个事件处理程序的单线程, 缓存Payload是完全安全的.

第 3 步: 使用 Infix 函数增加语法糖

在下一步中, 我们将把语法的表现力和可读性提升到一个新的水平. 我们引入了一个名为to的infix函数, 让我们可以优雅地将事件类映射到相应的动作.

请看更新后的代码:

abstract class EventsHandler(
val fillPayload: (cache: Payloads) -> Unit
) {
infix fun KClass.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(manager) {
lateinit var engine: Engine
lateinit var block: DesignBlock
lateinit var fontFamilyMap: Map

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)
}
// ...
}
}

语法优美和性能

infix函数to的引入为语法增添了一抹亮色, 增强了代码的表现力, 使使用更加自然. 这使得每个事件的内容一目了然. 不用担心, 得益于我们值得信赖的HashMap, 性能仍然保持在极快的(O(1)).

语法的灵活性

虽然这里使用了to关键字, 但你也可以用其他术语来代替它, 如handle,trigger或任何最适合你的上下文的术语. 灵活性就是本游戏的名字.

第 4 步: 使用内联函数以保持优雅

然而, 这仍然不够完美, 因为::class破坏了阅读的流畅性.

因此, 让我们换一种方式. 让我们尝试引入一种更优雅的方法来注册事件. 让我们不再需要每次注册事件处理程序时都指定::class, 这将使我们的代码更加简洁易读.

这可以通过一个内联函数来实现, 该函数带有一个经过验证的类型参数, 可在运行时维护类引用.

为此, 我们用这个新的register函数扩展了EventsHandler类:

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

新语法

这就是使用新语法注册事件处理程序的效果:

register {
engine.block.setWidth(block, engine.block.getFrameWidth(block))
engine.block.setHeight(block, it.width)
}

好多了, 是不是? 新的语法更加简洁, 消除了冗余, 而且是类型安全的, 因为重新定义的类型参数确保了事件类型在编译时和运行时都是已知的, 从而消除了不安全铸造的需要.

步骤 5: 将register提升为高亮显示的扩展函数

为了提高代码的可读性, 我们将采取一个微妙但有效的步骤, 将register函数从一个EventsHandler类函数转换为一个EventsHandler扩展函数.

听起来很蠢! 为什么呢?

这个小改动通过语法高亮显示 Kotlin 扩展函数中的register关键字, 提高了代码的可读性. 这将使代码更加丰富多彩, 从而提高可读性.

更新了EventsHandler

EventsHandler类在很大程度上保持不变, 但register函数现在在类之外, 并转化为EventsHandler类的扩展函数`:

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

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

只需将register移出类,EventsHandler类的定义就会以独特的语法高亮显示出来. 这是一个巧妙的技巧, 不会影响运行时或编译期的性能, 因为这是一个内联操作.

register {
engine.block.setWidth(block, engine.block.getFrameWidth(block))
engine.block.setHeight(block, it.width)
}

第 6 步: 使用委托属性消除lateinit变量

现在, 是时候解决神秘的lateinit变量和有点复杂的fillPayload机制了. 让我们引入一种更简洁的方法, 使用委托属性和 lambda 函数来注入依赖关系.

让我们添加一个Inject类, 将普通的 lambda 包装成可委托的:

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

有了这种新发现的能力, 我们的事件处理程序代码变得更简洁, 更直观. 它采用了 Jetpack Compose 的声明式语法:

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

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

每当访问其中一个变量时, lambda 就会被调用, 并始终获得当前变量.

此外, "payload"的创建也变得更加直接, 简洁和类型安全. 看起来有点像传递变量:

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 { engine.delete(block) }

register { engine.sendBackward(block) }

register { engine.duplicate(block) }

register { engine.bringForward(block) }

register { engine.sendToBack(block) }

register { engine.bringToFront(block) }

register { engine.editor.addUndoStep() }

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

register { 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,
) {
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 代码重构之旅的时候, 我们已经揭开了增强性能和风格的神秘面纱. 通过采用HashMap, infix 函数和带有重定义类型参数的内联函数等技术, 我们将代码提升到了新的高度. 这样做的好处显而易见: 提高了效率, 可读性并遵循了单一责任原则. 有了这些工具, 你现在就可以开始自己的编码冒险, 将凌乱的代码变成优雅的杰作.

Stay GOLD!