Bilateral Extensible Hierarchy with Java

My question is to implement various behaviors for different messages as broadly as possible. I know about the visitor pattern, I know about double dispatch, but I cannot find a solution that satisfies me (not within java at least).

My situation is this:

I have a message hierarchy:

Message hierarchy

and a hierarchy of router interfaces, each of which defines a route method for its own message type:

Router interface interface

which I would like to implement similarly to this:

Implementation

to be able to add and remove the ability to route specific messages, and to easily change routing strategies for specific messages.

The problem is that without switching my message, which I don’t want to do, I cannot select the appropriate function for the interface, because something like

CompositeRouter comp = new AllRouter(...//new Router instances); MessageBase msg = new DerivedMessage(); msg.process(comp); 

will select java overload <runtime message-type>.process(Router)

at compile time, which at run time is called for the corresponding router object. Therefore, I cannot select the correct process () calls at compile time. I also can't do it the other way around because comp.route(msg)

will be resolved to <dynamic router-type>.route(MessageBase) .

I could write a visitor who selects the correct method from CompositeRouter, but for this I would need to define a visitor interface with the appropriate route methods defined for all MessageTypes types in front, which type wins the target, because it means I have to rewrite the visitor whenever I add a new DerivedMessage.

Is there a way to implement this so that both Message and Router are extensible or is it hopeless given the current java functions?

Change 1:

Something I forgot to mention is that I have 4 or 5 other situations that pretty much match the Router hierarchy, so I seem to want to avoid Reflection for finding the method, because I'm afraid of the time consuming execution.

Reply to comments:

  • @aruisdante's assumption regarding @bot's suggestion is correct. I cannot redefine because I would lose the MessageBase runtime type if I redefine the route (MessageBase).

  • @aruisdante and @geceo: I know I can do this - this is what I meant with "switching" (MessageBase has a MessageType field) - but I have 11 actual message classes and ~ 6 locations in the code where I need it, so it will be a HUGE realization of pain as well as service wisely.

+6
source share
3 answers

Here's how I used to solve such problems in the past:

First, in your Router interface, since it seems that you intend to implement most of the Router implementations, except that the Composite processes only one type of message, change the interface definition to something similar to:

 interface Router<T extends MessageBase> { void route(T message); } 

This eliminates the need to provide interfaces for the various Router that handle specific implementations. After that, you got the Router classes:

 class OtherDerivedRouter implements Router<OtherDerivedMessage> { @Override void route(OtherDerivedMessage message) { //... }; } 

So what is going on in the 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 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 } } 

RouterAdaptor makes the heavy lifting of sending the correct type of message expected by the Router implementation it is running. And leaves CompositeRouter only need to save the registry of these adapters for its message type.

The biggest drawback of this approach is that with Type Erasure it’s not possible to create a Router implementation that processes more than one type of message on its own. From a Java perspective, at runtime, Router<MessageBase> same as Router<OtherDerivedMessage> , and therefore it is illegal to have something like SuperRouter implements Router<MessageBase>, Router<OtherDerivedMessage> , unlike C ++ templates. This is why you need to pass explisit Class<T> objects, and not just infer the type directly from Router<T> .

+4
source

you may have a "registry" of the Router implementation and the corresponding message types.

 class CompositeRouter implements Router { private Map<Class,Router> registry = new HashMap<>() void registerRouter(Class<? extends MessageBase> messageClass, Router router) { register.put(messageClass, router); } @Override void process(MessageBase message) { // here you can implement more sophisticated logic // to find most appropriate Router for given message according // type hierarchy Router router = registry.get(message.getClass()); router.process(message); } } 
+2
source

In Scala (another JVM language), this sounds like a usage example for type classes:

http://danielwestheide.com/blog/2013/02/06/the-neophytes-guide-to-scala-part-12-type-classes.html

This is not possible in Java, although there are several experimental libraries for it if you use “Java type classes”.

+1
source

Source: https://habr.com/ru/post/983433/


All Articles