My question is about implementing different behaviours for different messages in an as extensible way as possible. I am aware of the visitor pattern, I am aware of double-dispatch, but I can't seem to figure out a solution, which satiesfies me (not within the limits of java at least).
My situation is as follows:
I have a hierarchy of Messages:
and a hierarchy of router-interfaces, each defining a route method for its own message-type:
which I would like to implement similar to this:
to be able to add and remove the capability to route certain messages, as well as to change routing-strategies for certain messages easily.
The problem is, that without switch-casting my message, which I don't want to do, I cannot select the respective function for the interface, because something like
CompositeRouter comp = new AllRouter(...//new Router instances);
MessageBase msg = new DerivedMessage();
msg.process(comp);
will lead to java selecting the overload <runtime message-type>.process(Router)
at compile time, which, at runtime, is invoked for the respective router object. So I cannot select the right calls to process() at compile time it seems. I can also not do it the other way round, because comp.route(msg)
will be resolved to <dynamic router-type>.route(MessageBase)
.
I could write a visitor, which selects the proper method from CompositeRouter, but therefor I would have to define the visitor interface with the respective route-Methods defined for all the MessageTypes up front, which kind of defeats the purpose, because it means that I have to rewrite the visitor whenever I add a new DerivedMessage.
Is there a way to implement this such that both Message and Router are extensible or is it hopeless given the current java-features?
Edit 1:
Something I forgot to mention is that I have 4 or 5 other situations, which are pretty much the same as the Router
-hierarchy, so I kind of want to avoid Reflection for method-lookup, because I am afraid of the runtime-cost.
Response to comments:
@aruisdante's assumption regarding @bot's suggestion is correct. I cannot Override, because I would loose the runtime-type of MessageBase, if I override route(MessageBase).
@aruisdante and @geceo: I know that I can do that - this what I meant with "switch-casting" (MessageBase has a MessageType field) - but I have like 11 actual message classes and ~6 locations in code where I need it, so it would be a HUGE pain implementation- as well as maintenance-wise.
Here is how I've typically solved problems like this in the past:
First, in your Router
interface, since it seems you intend most Router
implementations with the exception of the Composite to handle only a single message type, change the definition of the interface to something similar to:
interface Router<T extends MessageBase> {
void route(T message);
}
This removes the need to provide interfaces for the various Router
s that handle specific implementations. Your derived Router
classes then become something like:
class OtherDerivedRouter implements Router<OtherDerivedMessage> {
@Override
void route(OtherDerivedMessage message) { //... };
}
So now what happens in CompositeRouter
? Well, we do something like this:
class CompositeRouter implements Router<MessageBase> {
protected static class RouterAdaptor< T extends MessageBase> implements Router<MessageBase> {
private Router<T> router;
private Class<T> klass;
RouterAdaptor(Router<T> router, Class<T> klass) {
this.router = router;
this.klass = klass;
}
@Override
public void route(MessageBase message) {
try {
router.route(klass.cast(message));
} (catch ClassCastException e) {
// Do whatever, something's gone wrong if this happens
}
}
}
private Map<Class<?>, RouterAdaptor<?>> routerMap;
@Override
public void route(MessageBase message) {
RouterAdaptor<?> adaptor = routerMap.get(message.getClass());
if (adaptor != null) {
adaptor.route(message)
} else {
// do your default routing case here
}
}
public <T extends MessageBase> void registerRouter(Router<T> router, Class<T> klass) {
// Right now don't check for overwrite of existing registration, could do so here
routerMap.put(klass, new RouterAdaptor<T>(router, kass));
}
CompositeRouter(/*...*/) {
//initialize routerMap with Map type of choice, etc
}
}
The RouterAdaptor
does the heavy lifting of dispatching the correct message type expected by the Router
implementation it holds. And leaves CompositeRouter
needing only to store a registry of these adaptors to their message type.
The biggest downside of this approach is that, thanks to Type Erasure, there is no way to create a Router
implementation that handles more than one message type by itself directly. From Java's prospective, at runtime Router<MessageBase>
is the same as Router<OtherDerivedMessage>
, and thus it is illegal to have something like SuperRouter implements Router<MessageBase>, Router<OtherDerivedMessage>
, unlike you could with C++ templates. This is also why you need to pass explisit Class<T>
objects around rather than just being able to infer the type directly from Router<T>
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With