Malcolm Crum
Malcolm Crum

Reputation: 4879

Elegant event handler with generics in Kotlin

I'm trying to build an event handler with events indexed by class. You would register to listen for events of class InterestingEvent : Event (for example) and would be notified whenever a new InterestingEvent() was sent to the manager.

I can't get the Kotlin generic code to work right though. This is the best I can do so far:

class EventManager{
    private val listeners: MutableMap<KClass<out Event>, MutableList<EventListener<in Event>>> = HashMap()

    fun <T : Event> register(event: KClass<out T>, listener: EventListener<T>) {
        val eventListeners: MutableList<EventListener<T>> = listeners.getOrPut(event) { ArrayList() }
        eventListeners.add(listener)
    }

    fun notify(event: Event) {
        listeners[event::class]?.forEach { it.handle(event) }
    }
}

My getOrPut call wants a MutableList<EventListener<T>> but found MutableList<EventListener<in Event>> instead.

Ideally I'd get rid of the KClass parameter to register as well but I don't think that's possible.

Is it possible to do what I'm trying here?

Upvotes: 2

Views: 6594

Answers (2)

Malcolm Crum
Malcolm Crum

Reputation: 4879

Pavlov Liapota also answered my question in the official Kotlin slack. Here is his solution:

class EventManager{
    private val listeners: MutableMap<KClass<out Event>, MutableList<EventListener<Event>>> = HashMap()

    inline fun <reified T : Event> register(listener: EventListener<T>) {
        register(T::class, listener)
    }

    fun <T : Event> register(eventClass: KClass<out T>, listener: EventListener<T>) {
        val eventListeners = listeners.getOrPut(eventClass) { ArrayList() }
        eventListeners.add(listener as EventListener<Event>)
    }

    fun notify(event: Event) {
        listeners[event::class]?.forEach { it.handle(event) }
    }
}

Upvotes: 2

Jayson Minard
Jayson Minard

Reputation: 85966

This is a bit difficult because you want to store a mix of EventListener<T: Event> items in lists within the same map, so that T is unknown for any given list. Then you want to use the items from the list as if T is known so it can be passed to notify(). So this causes trouble. A lot of libraries just notify the generic event and the receiver does the casting work. But you can do it in a few ways to take that burden into your notification library instead since it knows that logically there is a guarantee of the relationship between the key of the map and the list item types.

Here is some adjusted code with two forms of the notify.

class EventManager {
    val listeners: MutableMap<KClass<*>, MutableList<EventListener<out Event>>> = mutableMapOf()

    inline fun <reified T : Event> register(listener: EventListener<T>) {
        val eventClass = T::class
        val eventListeners: MutableList<EventListener<out Event>> = listeners.getOrPut(eventClass) { mutableListOf() }
        eventListeners.add(listener)
    }

    inline fun <reified T: Event> notify(event: T) {
        // here we have an unsafe action of going from unknown EventListener<T: Event> to EventListener<R>
        // which we know it is because of our code logic, but the compiler cannot know this for sure
        listeners[event::class]?.asSequence()
                               ?.filterIsInstance<EventListener<T>>() // or cast each item in a map() call
                               ?.forEach { it.handle(event) }
    }

    // or if you don't know the event type, this is also very unsafe
    fun notifyUnknown(event: Event) {
        listeners[event::class]?.asSequence()
                               ?.filterIsInstance<EventListener<Event>>()
                               ?.forEach { it.handle(event) }
    }
}

Given some simple sample classes:

open class Event(val id: String)
class DogEvent : Event("dog")
class CatEvent: Event("cat")

interface EventListener<T: Event> {
    fun handle(event: T): Unit
}

The following test works:

fun main() {
    val mgr = EventManager()

    mgr.register(object : EventListener<DogEvent> {
        override fun handle(event: DogEvent) {
            println("dog ${event.id}")
        }
    })

    mgr.register(object : EventListener<CatEvent> {
        override fun handle(event: CatEvent) {
            println("cat ${event.id}")
        }
    })

    mgr.notify(Event("nothing"))  // prints:  <nothing prints>
    mgr.notify(CatEvent())        // prints: cat cat
    mgr.notify(DogEvent())        // prints: dog dog

    mgr.notifyUnknown(CatEvent()) // prints: cat cat
    mgr.notifyUnknown(DogEvent()) // prints: dog dog
}

This works well since it is a self-contained system where the complete disregard we show for the type safety is not really exposed in dangerous ways and therefore is "safe enough." If I think of a better way, I'll update the posting. One method would be to create a new Map-of-class-to-lists that had a contract associated that tries to connect the value type with the key type and helps eliminate the casting.

Upvotes: 4

Related Questions