Search code examples
javareflectionvisitor-pattern

Dynamically choose method at runtime; alternatives to Visitor Pattern or Reflection


I'm working on a small game template, with a world comprised of nodes like so:

World
|--Zone
|----Cell
|------Actor
|------Actor
|--------Item

Where a World can contain multiple Zone objects, a Zone can contain multiple Cell objects, and so on.

Each of these implements the Node interface, which has a few methods like getParent, getChildren, update, reset and so on.

I want to be able to execute a given Task on a single node or recursively down the tree from a node (as specified by the Task).

To compound this issue, I would like this to be a "pluggable" system, meaning I want players/developers to be able to add new types to the tree on the fly. I had also considered casting from the base types:

public void doTask(Actor node)
{
    if(!(node instanceof Goblin)) { return; }
    Goblin goblin = (Goblin) node;
}

Initially I was drawn to use the Visitor Pattern to take advantage of double dispatch, allowing each routine (Visitor) to act according to the type of Node being visited. However, this caused a few complications, specifically when I want to add a new Node type to the tree.

As an alternative, I wrote a utility class that uses reflection to find the most specific method applicable to the Node.

My concern now is performance; since there will be a fairly large number of reflective lookups and calls, I'm worried that the performance of my game (which could have hundreds or thousands of these calls per second) will suffer.

Which seems to solve the problem of both patterns, but makes the code for each new Task uglier.

The way I see it, I have three options for allowing this dynamic dispatch (unless I'm missing something obvious/obscure, which is why I'm here):

  1. Visitor Pattern
    • Pros
      • Double Dispatch
      • Performance
      • Clean Code in tasks
    • Cons
      • Difficult to add new Node types (impossible without modifying original code)
      • Ugly code during invocation of tasks
  2. Dynamic Invocation using Reflection
    • Pros
      • Can add new Node types with abandon
      • Very customizable tasks
      • Clean Code in tasks
    • Cons
      • Poor performance
      • Ugly code during invocation of tasks
  3. Casting
    • Pros
      • More performant than reflection
      • Potentially more dynamic than Visitor
      • Clean code during invocation of tasks
    • Cons
      • Code smell
      • Less performant than Visitor (no double dispatch, casting in each invocation)
      • Ugly code in tasks

Have I missed something obvious here? I'm familiar with many of the Gang of Four patterns, as well as the ones in Game Programming Patterns. Any help would be appreciated here.

To be clear, I'm not asking which of these is the "best". I'm looking for an alternative to these approaches.


Solution

  • So after some research into Java 8 Lambdas and how they can be constructed reflectively, I came up with the idea of creating a BiConsumer from the Method object I had obtained reflectively, with the first argument being the instance on which the method should be invoked and the second argument being the actual argument of the method:

    private static <T, U> BiConsumer<T, U> createConsumer(Method method) throws Throwable {
        BiConsumer<T, U> consumer = null;
        final MethodHandles.Lookup caller = MethodHandles.lookup();
        final MethodType biConsumerType = MethodType.methodType(BiConsumer.class);
        final MethodHandle handle = caller.unreflect(method);
        final MethodType type = handle.type();
    
        CallSite callSite = LambdaMetafactory.metafactory(
              caller,
              "accept",
              biConsumerType,
              type.changeParameterType(0, Object.class).changeParameterType(1, Object.class),
              handle,
              type
        );
        MethodHandle factory = callSite.getTarget();
        try {
            //noinspection unchecked // This is manually checked with exception handling.
            consumer = (BiConsumer<T,U>) factory.invoke();
        }catch (ClassCastException e) {
            LOGGER.log(Level.WARNING, "Unable to cast to BiConsumer<T,U>", e);
        }
        return consumer;
    }
    

    Once this BiConsumer has been created, it is cached in a HashMap using the parameter type and method name as a key. It can then be invoked like so:

    consumer.accept(nodeTask, node);
    

    This method of invocation almost entirely eliminates the invocation overhead incurred by reflection, but it does have a couple of issues/constraints:

    • Because of the use of BiConsumer, only one parameter may be passed into the method (the first argument to the accept method must be the instance that should have the method invoked on it).
      • This is fine for my purposes, I only want to pass one argument anyway.
    • There is a non-trivial performance overhead when invoking a method with parameter types that haven't been seen before, as it must first be searched for reflectively.
      • Again, for my purposes this is OK; the number of acceptable node types will not be very large and will be quickly cached as they are seen. After this first "discovery" of the appropriate method for combinations of parameter types, the overhead is very small (constant, I believe, as it's a simple HashMap lookup).
    • Requires Java 8 (which I was using already anyway)

    I could clarify this code a bit by using a custom functional interface (something like an Invoker class instead of Java's BiConsumer) but as of right now it does exactly what I want with the performance I want.