In the world of software development, code refactoring is the hero that rescues us from tangled and inefficient code. In this article, we'll embark on an adventure to revamp Kotlin code handling diverse events. Our mission? To enhance performance and style, making the code sleeker, more maintainable, and a joy to work with.
What We Aim to Achieve
On this journey to transform Kotlin event handling, our goal is to refine our code to be more efficient, readable, and maintainable. We're introducing a variety of improvements, including:
- Replacing a convoluted
when
statement with aHashMap
for lightning-fast (O(1)) performance. - Infusing syntactic sweetness with inline functions and reified type parameters.
- Employing delegated properties for cleaner dependency injection.
- Adhering to the Single Responsibility Principle by enabling multiple specialized event handler functions.
Step 1: The Starting Line
Our adventure begins with a glance at the original code. This codebase manages a variety of block events through a function named handleBlockEvent
and an event handler function called onEvent
. Let's unveil the original code:
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...
}
To use the original code, you'd typically call the onEvent
function with a specific event:
onEvent(BlockEvent.OnChangeFillColor(Color.RED))
This would then trigger the handleBlockEvent
function to deal with the event at hand. Now, let's embark on our first refactoring adventure.
Step 2: Unveiling HashMaps and Payloads for Peak Performance
In our first act of refactoring, we introduce a trusty HashMap
to map each event type to its corresponding action. This heroic move eliminates the need for the convoluted when
statement, making our code more efficient. We also unveil a payload mechanism to convey essential data to the event handlers.
Behold the refactored code:
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)
}
A Performance Boost
By harnessing the power of a HashMap
, we've turbocharged our event handling. The time complexity for handling an event is now a lightning-fast (O(1)), a monumental improvement over the (O(n)) time complexity of the ponderous when
statement. While our payload mechanism adds a dollop of syntactic sugar. It enables us to bundle all the necessary data into a single object, making our code more legible and maintainable.
? Note: Using a HashMap instead of a large when() statement provides a significant performance improvement. It can be up to 40 to 150 times faster. However, explaining the details would exceed the scope of this blog post. Therefore, I will cover it, along with other Kotlin performance puzzles, in a future blog post.
While the refactored code remains as simple as before:
onEvent(BlockEvent.OnChangeFillColor(Color.RED))
This still triggers the handleEvent
method in BlockEventsHandler
, which in turn performs the appropriate action based on the event type. The BlockEvent
itself is a data object containing all event details, and it serves as the lambda parameter.
A Note on Payloads
The payload creation is a dynamic lambda function that's executed each time an event is handled. This ensures that all variables not part of the event are consistently up-to-date. Given that we're dealing with a single thread per event handler, caching the payload is entirely secure.
Step 3: Adding Syntactic Sweetness with Infix Functions
In our next act, we elevate our syntax to a new level of expressiveness and readability. We introduce an infix function called to
, allowing us to map an event class to its corresponding action elegantly.
Witness the updated code:
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)
}
// ...
}
}
Syntactic Sweetness and Performance
The introduction of the to
infix function adds a sprinkle of syntactic sweetness that enhances code expressiveness and enables a more natural usage. This makes it crystal clear what each event is all about. And fear not, the performance remains at a blazing-fast (O(1)), thanks to our trusty HashMap.
Flexibility in Syntax
While the to
keyword is used here, feel free to substitute it with other terms like handle
, trigger
, or anything that best suits your context. Flexibility is the name of the game.
Step 4: Embracing Inline Functions for Elegance
However, this is still not perfect because the ::class
breaks smooth reading.
So let's do it differently. Let us try to introduce a more elegant way to register an event. Let us eliminate the need to specify ::class
every time we register an event handler will make our code more concise and readable.
This is made possible by an inline function with a verified type parameter that maintains the class reference at runtime.
To do this, we extend the EventsHandler
class with this new register
function:
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)
}
The New Syntax
This is what registering an event handler looks like with the new syntax:
register<BlockEvent.OnChangeLineWidth> {
engine.block.setWidth(block, engine.block.getFrameWidth(block))
engine.block.setHeight(block, it.width)
}
Much better, right? The new syntax is more concise, eliminates redundancy, and is type-safe because the reified type parameters ensure that the event type is known at compile-time and runtime, eliminating the need for unsafe casting.
Step 5: Elevating register
to an Extension Function for Highlighting
To improve code readability, we'll make a subtle but effective step by converting the register
function from a EventsHandler
class function, into an EventsHandler
extension function.
Sounds stupid! So why?
This small change improves code readability by highlighting the register
keyword through syntax highlighting from a Kotlin extension function. This will make it much more colorful, which improves readability.
Updated EventsHandler
Class
The EventsHandler
class remains largely unchanged, but the register
function is now outside the class and transformed into an extension function for the EventsHandler
class:
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
}
By simply shifting register
out of the class, the EventsHandler
class definition now stands out with distinctive syntax highlighting. It's a clever trick that doesn't impact runtime or compile performance, since it's an inline operation anyway.
**register**<BlockEvent.OnChangeLineWidth> {
engine.block.setWidth(block, engine.block.getFrameWidth(block))
engine.block.setHeight(block, it.width)
}
Step 6: Eliminating lateinit
Variables with Delegated Properties
Now, it's time to address the enigmatic lateinit
variables and the somewhat convoluted fillPayload
mechanism. Let us introduce a cleaner approach, using delegated properties and lambda functions to inject dependencies.
Let's add an Inject
class to wrap a normal lambda as delegable:
class Inject<Type>(private val inject: () -> Type) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Type = inject()
}
With this newfound power, our event handler code becomes cleaner and more intuitive. It takes on the style of Jetpack Compose's declarative syntax:
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
// ...
}
Whenever one of the variables is accessed, the lambda is called, and you always get the current variable.
Also, the creation of the "payload" becomes more straightforward, clean, and type-safe. It kinda looks like passing a variable:
private val eventHandler = EventsHandler {
textBlockEvents (
engine = ::engine,
block = ::getBlockForEvents,
fontFamilyMap = { checkNotNull(assetsRepo.fontFamilies.value).getOrThrow() },
)
}
Looks and feels like magic! Pretty cool, right?
Step 7: Multiple Event Handlers for Single Responsibility Principle
In our grand finale, we harness the newfound flexibility from our previous changes to register multiple event handler functions. Each event handler registration function now has a specific topic, aligning perfectly with the Single Responsibility Principle (SRP).
Enhanced Event Handler Registration
We can now register multiple event handler functions within the same EventsHandler
instance. Each function can specialize in handling a particular type of event, making the code more modular and manageable. Behold the grand design:
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)
}
While the triggering and its API remain unchanged, and no extra parameters need to be passed:
open fun onEvent(event: Event) {
eventHandler.handleEvent(event)
}
Final Words
As we conclude our journey through Kotlin code refactoring, we've unlocked the secrets to enhanced performance and style. By embracing techniques such as HashMaps, infix functions, and inline functions with reified type parameters, we've elevated our code to new heights. The benefits are clear: improved efficiency, readability, and adherence to the Single Responsibility Principle. Armed with these tools, you're now ready to embark on your own coding adventures, transforming messy code into elegant masterpieces.
If you’d like to try it out, I’ve created a working example code on the Kotlin Playground.
Thank you for accompanying, and happy coding! Never miss out on updates and subscribe to our newsletter.