在运行时动态选择方法;访客模式或反思的替代方案

时间:2015-07-20 22:53:47

标签: java reflection visitor

我正在开发一个小型游戏模板,其中包含如下节点的世界:

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

World可以包含多个Zone个对象,Zone可以包含多个Cell个对象,依此类推。

其中每个都实现了Node界面,其中包含一些方法,如getParentgetChildrenupdatereset等等。

我希望能够在单个节点上执行给定的Task,或者从节点递归地执行树(由Task指定)。

为了解决这个问题,我希望这是一个“可插拔”系统,这意味着我希望玩家/开发人员能够动态地向树中添加新类型。我还考虑过基类型的转换:

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

最初我被吸引使用Visitor Pattern来利用双重调度,允许每个例程(访问者)根据被访问的Node的类型行事。但是,这会导致一些复杂情况,特别是当我想向树中添加新的Node类型时。

作为替代方案,我写了一个utility class,它使用反射来查找适用于Node的最具体方法。

My concern now is performance;因为会有相当多的反思性查询和调用,我担心我的游戏性能(每秒可能有数百或数千次这样的调用)会受到影响。

这似乎解决了这两种模式的问题,但却使每个新Task的代码更加丑陋。

我看到它的方式,我有三个选项允许这种动态调度(除非我遗漏了一些明显/模糊的东西,这就是为什么我在这里):

  1. 访客模式
    • 赞成
      • Double Dispatch
      • 性能
      • 清理任务中的代码
    • 缺点
      • 很难添加新的Node类型(无法修改原始代码)
      • 调用任务期间的丑陋代码
  2. 使用反射进行动态调用
    • 赞成
      • 可以添加新的Node类型并放弃
      • 非常可自定义的任务
      • 清理任务中的代码
    • 缺点
      • 表现不佳
      • 调用任务期间的丑陋代码
  3. 铸造
    • 赞成
      • 比反思更高效
      • 可能比访客
      • 更具活力
      • 在调用任务期间清理代码
    • 缺点
      • 代码味道
      • 性能低于访问者(没有双重调度,每次调用时都要进行投射)
      • 任务中的丑陋代码
  4. 我错过了一些明显的东西吗?我熟悉许多四人组模式,以及Game Programming Patterns中的模式。这里有任何帮助。

    要明确的是,我不是在问这些是“最好的”。我正在寻找这些方法的替代方案。

4 个答案:

答案 0 :(得分:3)

因此,在对Java 8 Lambdas进行一些研究以及如何反思构造之后,我提出了从我已经反射性地获得的BiConsumer对象创建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;
}

创建此BiConsumer后,使用参数类型和方法名称作为键将其缓存在HashMap中。然后可以像这样调用它:

consumer.accept(nodeTask, node);

这种调用方法几乎完全消除了反射引起的调用开销,但确实存在一些问题/约束:

  • 由于使用了BiConsumer,因此只能将一个参数传递给方法(accept方法的第一个参数必须是应该在其上调用方法的实例)。
    • 这对我来说很好,我只想传递一个参数。
  • 在调用具有之前未见过的参数类型的方法时,存在一个非常重要的性能开销,因为必须首先反复搜索它。
    • 同样,为了我的目的,这是可以的;可接受的节点类型的数量不会很大,并且会在看到它们时快速缓存。在对参数类型组合的适当方法的第一次“发现”之后,开销非常小(我相信,这是一个简单的HashMap查找的常量。)
  • 需要Java 8(我之前已经使用过)

我可以通过使用自定义功能接口(类似于Invoker类而不是Java的BiConsumer)来澄清这段代码,但是现在它完全符合我想要的性能I想。

答案 1 :(得分:1)

我认为如果你不能拥有静态工厂类,那么这是一个棘手的问题。如果允许静态工厂,那么这个简短的例子可能会提供一些想法。

这种方法允许将INode实例的运行时插入到树(WorldNode)中,但是,它并没有回答如何创建这些具体的INode。我希望你会有某种工厂模式。

    import java.util.Vector;

    public class World {

      public static void main(String[] args) {
        INode worldNode = new WorldNode();
        INode zoneNode = new ZoneNode();

        zoneNode.addNode(new GoblinNode());
        zoneNode.addNode(new GoblinNode());
        zoneNode.addNode(new GoblinNode());
        zoneNode.addNode(new GoblinNode());
        worldNode.addNode(zoneNode);

        worldNode.addNode(new ZoneNode());
        worldNode.addNode(new ZoneNode());
        worldNode.addNode(new ZoneNode());

        worldNode.runTasks(null);
      }
    }

    interface INode {
      public void addNode(INode node);
      public void addTask(ITask node);
      public Vector<ITask> getTasks();
      public void runTasks(INode parent);
      public Vector<INode> getNodes();
    }

    interface ITask {
      public void execute();
    }

    abstract class Node implements INode {
      private Vector<INode> nodes = new Vector<INode>();
      private Vector<ITask> tasks = new Vector<ITask>();

      public void addNode(INode node) {
        nodes.add(node);
      }

      public void addTask(ITask task) {
        tasks.add(task);
      }

      public Vector<ITask> getTasks() {
        return tasks;
      }

      public Vector<INode> getNodes() {
        return nodes;
      }

      public void runTasks(INode parent) {
        for(ITask task : tasks) {
          task.execute();
        }
        for(INode node : nodes){
          node.runTasks(this);
        }
      }
    }

    class WorldNode extends Node {
      public WorldNode() {
        addTask(new WorldTask());
      }
    }

    class WorldTask implements ITask {
      @Override
      public void execute() {
        System.out.println("World Task");
      }
    }

    class ZoneNode extends Node {
      public ZoneNode() {
        addTask(new ZoneTask());
      }
    }

    class ZoneTask implements ITask {

      @Override
      public void execute() {
        System.out.println("Zone Task");
      }
    }

    class GoblinNode extends Node {
      public GoblinNode() {
        addTask(new GoblinTask());
      }
    }

    class GoblinTask implements ITask {

      @Override
      public void execute() {
        System.out.println("Goblin Task");
      }
    }

输出:

World Task
    Zone Task
        Goblin Task
        Goblin Task
        Goblin Task
        Goblin Task
Zone Task
Zone Task
Zone Task

答案 2 :(得分:1)

反思的想法很好 - 你只需要根据参数类型缓存查找结果。

访问者模式可以通过用户程序扩展。例如,给定访问者模式中的经典NodeVisitor定义,用户可以定义 MyNode, MyVisitor

interface MyVisitor extends Visitor
{
    void visit(MyNode m);
    void visit(MyNodeX x);
    ...
}

interface MyNode extends Node
{
    @Override default void accept(Visitor visitor)
    {
        if(visitor instanceof MyVisitor)
            acceptNew((MyVisitor) visitor);
        else
            acceptOld(visitor);
    }

    void acceptNew(MyVisitor visitor);
    void acceptOld(Visitor visitor);
}

class MyNodeX implements MyNode
{
    @Override public void acceptNew(MyVisitor visitor)
    {
        visitor.visit(this);
    }
    @Override public void acceptOld(Visitor visitor)
    {
        visitor.visit(this);
    }
}
// problematic if MyNodeX extends NodeX; requires more thinking

一般来说,我不喜欢访客模式;它是相当丑陋,僵硬和侵入性的。

基本上,问题在于给定节点类型和任务类型,查找处理程序。我们可以通过(node,task)->handler的简单地图来解决这个问题。我们可以为绑定/查找处理程序创建一些API

register(NodeX.class, TaskY.class, (x,y)->
{ 
    ...  
});

或匿名类

new Handler<NodeX, TaskY>()  // the constructor registers `this`
{
    @Override public void handle(NodeX x, TaskY y)
    ...

要在节点上调用任务,

invoke(node, task);
// lookup a handler based on (node.class, task.class)
// if not found, lookup a handler on supertype(s). cache it by (node.class, task.class)

答案 3 :(得分:0)

如果您正在寻找表现 - 访客模式是可行的方式。我个人甚至不会想出反射作为解决方案,因为它似乎不真实和过于复杂。转换工作,但在OO环境中,它通常被认为是代码气味,应该避免(例如使用访问者模式)。

另一个重要方面是可读性 vs 可写性:访问者模式可能需要更多工作,并且在添加节点时需要更多维护,但它绝对更容易阅读和通常也更容易理解。反思在两个方面都是禁忌,而演员也是代码嗅觉。