Solution: Correct EventListenerRegistry

The first problem is that the handler variable in the EventListener class is not cleared when the listener is canceled. It might be a heavy object, which is why we should clear it. We can do this by setting handler to null in the cancel method.

The second, smaller problem is that the listeners variable in EventListenerRegistry does not eliminate canceled listeners. We can do this by removing canceled listeners from the set when listeners are invoked.

class EventListenerRegistry<E> { private var listeners = ConcurrentHashMap .newKeySet<EventListener<E>>() private val lock = Any() fun addEventListener( event: E, handler: () -> Unit ): EventListener<E> = synchronized(lock) { val listener = EventListener(event, handler) listeners += listener listener } fun invokeListeners(event: E) { val eventListeners = listeners .filter { it.event == event } for (listener in eventListeners) { if (listener.isActive) { listener.handleEvent() } else { listeners.remove(listener) } } } } class EventListener<E>( val event: E, handler: () -> Unit, ) { private var handler: (() -> Unit)? = handler val isActive: Boolean get() = handler != null fun handleEvent() { handler?.invoke() } fun cancel() { handler = null } }

This problem can also be solved by removing canceled listeners from a set immediately after canceling them, but this solution would require event listeners to have access to the repository, which might be problematic in some cases.

Example solution in playground

import org.junit.Test import java.util.concurrent.ConcurrentHashMap import kotlin.test.assertEquals class EventListenerRegistry<E> { private var listeners = ConcurrentHashMap .newKeySet<EventListener<E>>() private val lock = Any() fun addEventListener( event: E, handler: () -> Unit ): EventListener<E> = synchronized(lock) { val listener = EventListener(event, handler) listeners += listener listener } fun invokeListeners(event: E) { val eventListeners = listeners .filter { it.event == event } for (listener in eventListeners) { if (listener.isActive) { listener.handleEvent() } else { listeners.remove(listener) } } } } class EventListener<E>( val event: E, handler: () -> Unit, ) { private var handler: (() -> Unit)? = handler val isActive: Boolean get() = handler != null fun handleEvent() { handler?.invoke() } fun cancel() { handler = null } } enum class Event { A, B, C } class EventListenerRegistryTest { @Test fun `should invoke proper handlers`() { val eventListenerRepository = EventListenerRegistry<Event>() var a = 0 var b = 0 var c = 0 eventListenerRepository.addEventListener(Event.A) { a++ } eventListenerRepository.addEventListener(Event.B) { b++ } eventListenerRepository.addEventListener(Event.C) { c++ } assertEquals(0, a) assertEquals(0, b) assertEquals(0, c) eventListenerRepository.invokeListeners(Event.A) assertEquals(1, a) assertEquals(0, b) assertEquals(0, c) eventListenerRepository.invokeListeners(Event.B) eventListenerRepository.invokeListeners(Event.B) assertEquals(1, a) assertEquals(2, b) assertEquals(0, c) eventListenerRepository.invokeListeners(Event.C) eventListenerRepository.invokeListeners(Event.C) eventListenerRepository.invokeListeners(Event.C) assertEquals(1, a) assertEquals(2, b) assertEquals(3, c) } @Test fun `should allow setting more than one handler for an event`() { val eventListenerRepository = EventListenerRegistry<Event>() var a = 0 var b = 0 var c = 0 eventListenerRepository.addEventListener(Event.A) { a++ } eventListenerRepository.addEventListener(Event.A) { b++ } eventListenerRepository.addEventListener(Event.A) { c++ } eventListenerRepository.invokeListeners(Event.A) assertEquals(1, a) assertEquals(1, b) assertEquals(1, c) } @Test fun `should allow listener cancelation`() { val eventListenerRepository = EventListenerRegistry<Event>() var a = 0 val listener = eventListenerRepository.addEventListener(Event.A) { a++ } listener.cancel() eventListenerRepository.invokeListeners(Event.A) assertEquals(0, a) } }