Two-way extensible hierarchy with Java
Asked Answered
S

3

6

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:

Message Hierarchy

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

Router-Interface Hierarchy

which I would like to implement similar to this:

Implementation

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:

  1. @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).

  2. @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.

Spinescent answered 6/3, 2015 at 15:44 Comment(6)
I may have not understood your problem correctly but why are you 'overloading' the methods instead of 'overriding' them? Why pass the messages as parameters? Why not have MessageBase as a dependency in Router instead?Izabel
@bot I think the OP's intention is to allow for handling different messages in different ways (overloading), and to also allow different classes to handle different messages in different ways (overriding), but not to require every class in the hierarchy to have to provide all of the overloads. The problem they are running into is essentially for the system to work as designed, both the message and the router must know the concrete runtime type of each other (message needs to know the type of router, router needs to know type of message).Tirpitz
I once wrote some limited "generic function" stuff for Java. Note, that the code is neither well-tested, nor maintained, let alone documented or supported. I do not recommend using it in production environments, but maybe you can use it to get some inspiration...Atrip
Unfortunately in Java I don't think there is going to be a way to avoid using instanceof with your constraints. Most generic message passing systems I've worked with in Java, for example Akka, have a single method similar to your route, and SOP is to instanceof check the incoming messages and dispatch accordingly. You could provide some common functionality in your base Router class to abstract this so that subclasses register callbacks for concrete classes to simulate the double-dispatch. If you're open to that I can provide an example as an answer.Tirpitz
@Spinescent : I agree with @aruisdante. I had a similar situation with Java seeing objects through their common interface at runtime, even though I had written methods for each specific concrete class implementing the interface. I couldn't find a better solution than to use instanceof. It's not pretty but it is simple.Geyer
Dynamic lookup of capabilities can be done with embedding rather than inheritance. The lookup mechanism itself can be inherited, extended, redone.Stricker
T
4

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 Routers 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>.

Tirpitz answered 6/3, 2015 at 17:15 Comment(0)
W
2

you can have a 'registry' of Router implemetations and their 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);
   } 
} 
Wondawonder answered 6/3, 2015 at 16:27 Comment(0)
R
1

In Scala (another JVM language), this sounds like a use case 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, though there are some experimental libraries for it if you google "Java type classes".

Ranique answered 6/3, 2015 at 16:3 Comment(1)
Sorry, but it has to be Java. I think I would know a hack to make it work in C++ too, but just as much as Scala it is out of question.Spinescent

© 2022 - 2024 — McMap. All rights reserved.