Struggling how to do it in spring: I have a light...
# spring
p
Struggling how to do it in spring: I have a lightweight object (a POJO with just 1-2 fields, it is received as an "Event"). Those objects are under my control: Base class with known descendants. The hard part: Based on the concrete subtype of Event, I should do some calculations, which result in say: a number. But to do the calculations, I need different services, repositories, whatever. So my question: How to guarantee, that every subclass of Event implements this calculation? "Of course" without a convoluted switch-case in a god like Service class knowing about every repository on Earth? See the "bad" solution in the snippet.
h
You could have Spring create your `Event`s too (prototype scope) and inject the necessary dependencies. Via the default reflection that's pretty slow though.
🐌 1
s
In such a situation I would use a strategy pattern: Here's my example solution: I would first declare generic interface describing the action you want to achieve on event of given type, kind of like this:
Copy code
interface 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:
Copy code
@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:
Copy code
@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:
Copy code
@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-site
p
How will be
eventProcessors
injected to
EventStrategyDelegator
?
s
Sorry, missed the mention about thread reply. Injection can be done exactly as shown on the example. Spring can handle injecting multiple beans of given types as a collection. Having
eventProcessors: 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 type
p
Thanks, I was not aware of that
List<...>
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.