I would like to create a simple API for processing in-app events (sending their info to various targets).
I would like to have that type-based, so that the event processor would know where to send which event would be determined by the sub-type of the event.
Here is an example:
interface AppEvent
interface Sender<T: AppEvent> {
fun send(event: T)
val acceptsAbility: KClass<T>
}
class Loggable: AppEvent, HasMessage
interface HasMessage { val message: String }
class LogSender : Sender<Loggable> {
override fun send(event: Loggable) = Logger(...).info(event.message)
override val acceptsAbility = Loggable::class
}
fun main() {
val dispatcher = Dispatcher<AppEvent>()
val sender: Sender<Loggable> = LogSender()
dispatcher.addSender(sender)
}
A particular event should then be a mixture of various ...able
interfaces, which would determine, which all Sender
s would send them. This would be used by the dispatcher class: If the given event matches the interface of some sender, then pass it to that sender. In Kotlin, this needs to be checked at runtime.
class ImportErrorEvent : AppEvent, Loggable
Note to that - I would prefer Loggable
not to inherit from AppEvent
, but that would need the TypeScript way of type compatibility, where just having the right mix of properties and functions is enough to match the type requirement.
So I attempted to code a basic dispatcher, but ran into type generics issues.
class Dispatcher <T: AppEvent> {
val senders: MutableList<Sender<out T>> = mutableListOf()
fun <TF: Sender<out T>> addSender(a: TF) { senders.add(a) }
fun dispatch(event: T) {
if (event is sender.acceptedAbility)
senders
.filter { it.acceptsAbility.isInstance(event) }
.forEach { it.send(event as AppEvent)} // This type check needs to be relaxed.
}
}
class Dispatcher2 {
val senders: MutableList<Sender<out AppEvent>> = mutableListOf()
fun <T: AppEvent, TS: Sender<out T>> addSender(a: TS) { senders.add(a) }
fun dispatch(event: Any) {
senders
.filter { it.acceptsAbility.isInstance(event) }
.forEach { it.send(event as AppEvent)} // This type check needs to be relaxed.
}
}
Both of these have a compilation error in senders.forEach { it.send(event) }
:
Type mismatch.
Required: Nothing
Found: Any
I have the same idea implemented in Java using extends
and super
.
What would be the right way to code this idea with generic?
Is there some extended documentation on Kotlin generics I could dive into?
Edit:
The type safety is intended to be partially done at runtime:
TheDispatcher
would
Loggable
),I have changed the code above to include the type check.
So I am looking for a way to relax the type checking while still imposing some constraints for the calling code.
If I understand you, you have to write something like this:
interface AppEvent
interface Sender<in T : AppEvent> {
fun send(event: T)
}
interface HasMessage {
val message: String
}
class Loggable : AppEvent, HasMessage {
override val message: String = ""
}
class Testable : AppEvent
class LogSender : Sender<Loggable> {
override fun send(event: Loggable) = Unit
}
fun main() {
val station = Dispatcher()
val feeder: Sender<Loggable> = LogSender()
station.addSender(feeder)
station.dispatch(Loggable()) // It will be send
station.dispatch(Testable()) // It will not send
}
class Dispatcher {
private val senders = mutableListOf<Sender<AppEvent>>()
inline fun <reified T : AppEvent> addSender(sender: Sender<T>) {
val wrapper: Sender<AppEvent> = object : Sender<AppEvent> {
override fun send(event: AppEvent) {
if (T::class.isInstance(event)) {
sender.send(event as T)
}
}
}
addCommonSender(wrapper)
}
fun addCommonSender(sender: Sender<AppEvent>) {
senders.add(sender)
}
fun dispatch(event: AppEvent) {
for (sender in senders) {
sender.send(event)
}
}
}
inline + reified allows you to capture sender's type of T. After it you can create wrapper which will implements Sender and cast it to T after runtime check.
Finally you have type independent dispatcher and strongly typed senders.
The other option it's strongly typed dispatcher:
class Dispatcher2 <T: AppEvent> {
val senders: MutableList<Sender<T>> = mutableListOf()
fun addSender(a: Sender<T>) { senders.add(a) }
fun dispatch(event: T) {
for(sender in senders) {
sender.send(event)
}
}
}
It could be used like this:
fun main() {
val station = Dispatcher2<Loggable>()
val feeder: Sender<Loggable> = LogSender()
station.addSender(feeder)
station.dispatch(Loggable()) // Dispatched
station.dispatch(ImportErrorEvent()) // Dispatched too
}