Pihentagy
07/08/2024, 1:26 PMhho
07/08/2024, 4:59 PMSzymon Jeziorski
07/09/2024, 8:12 AMinterface EventProcessor<T : Event> {
val supportedClass: KClass<T>
fun calculate(event: T): Int
}
supportedClass
here works as a discriminator - we want to have a field which would allow us to determine whether given processor is applicable to given event. It's a class reference here, but it could be anything by which events can be distinguished (for example it could be an enum value if each event had enum field as a discriminator)
We would have different implementations for different events, for example:
@Service
class Event1Processor(
private val injectedBean1,
private val injectedBean2,
[...]
) : EventProcessor<Event1> {
override val supportedClass = Event1::class
override fun calculate(event: Event1): Int {
... // calculate on Event1
}
}
[...]
@Service
class Event2Processor(
private val injectedBean3,
[...]
) : EventProcessor<Event2> {
override val supportedClass = Event2::class
override fun calculate(event: Event1): Int {
... // calculate on Event2
}
}
Then, I would create a strategy pattern wrapper, which would inject all of those implementations and provide a single method to return proper implementation:
@Service
class EventStrategyDelegator(
eventProcessors: List<EventProcessor<*>>,
) {
private val processorsByClass = eventProcessors.associateBy { it.supportedClass }
@Suppress("UNCHECKED_CAST")
fun <T : Event> getProcessor(event: T): EventProcessor<T> =
processorsByClass.entries
.find { it.value.supportedClass.isInstance(event) }
?.let { it.value as EventProcessor<T> }
?: error("Handle error case")
}
And with above, I would just inject strategy and use it to return proper implementation when I want to apply generic processing:
@Service
class CallSite(
private val eventStrategyDelegator: EventStrategyDelegator
) {
fun processEvent(event: Event) {
val calculationResult = eventStrategyDelegator.getProcessor(event).calculate(event)
}
}
Major benefit of such an approach is that when you suddenly have new event type to handle, you only need to register a bean for a processor for new event (with correct discriminator).
Entire code in strategy handler (here EventStrategyDelegator
) and calling site (here CallSite
) would stay untouched - new implementation would be automatically injected into the delegator and the delegator would just return new implementation following the same contract declared in the interface when called by the call-sitePihentagy
07/11/2024, 2:53 PMeventProcessors
injected to EventStrategyDelegator
?Szymon Jeziorski
07/15/2024, 8:24 AMeventProcessors: List<EventProcessor<*>>
within primary constructor of a bean automatically managed by Spring (for example annotated with Service/Component/Repository
), would automatically inject all beans of type EventProcessor<*>
as a List. And because of the wildcard, all EventProcessor
beans will be injected regardless of their generic typePihentagy
07/15/2024, 9:32 AMList<...>
trick.
Seems like if I split the processor and the Events, I will need to check upon startup time if all subclasses are covered with appropriate processors.