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

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

我正在制作一个小型游戏模板,其中的世界由如下节点组成:

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

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

其中每一个都实现了 Node 接口,它有一些方法,如 getParentgetChildrenupdatereset 等。

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

为了解决这个问题,我希望这是一个 "pluggable" 系统,这意味着我希望 players/developers 能够动态地向树中添加新类型。我还考虑过从基本类型进行转换:

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

最初我被吸引使用Visitor Pattern来利用双重分派,允许每个例程(Visitor)根据被访问的Node的类型来行动。但是,这引起了一些并发症,特别是当我想向树中添加新的 Node 类型时。

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

My concern now is performance;由于会有相当多的反射查找和调用,我担心我的游戏性能(每秒可能有成百上千次这样的调用)会受到影响。

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

在我看来,我有三个选项来允许这种动态调度(除非我遗漏了什么 obvious/obscure,这就是我来这里的原因):

  1. 访客模式
    • 优点
      • 双重派遣
      • 性能
      • 清理任务中的代码
    • 缺点
      • 难以添加新的 Node 类型(不修改原始代码是不可能的)
      • 调用任务期间的丑陋代码
  2. 使用反射的动态调用
    • 优点
      • 可以使用 abandon
      • 添加新的 Node 类型
      • 高度可定制的任务
      • 清理任务中的代码
    • 缺点
      • 表现不佳
      • 调用任务期间的丑陋代码
  3. 铸造
    • 优点
      • 比反射更高效
      • 可能比访客更动态
      • 调用任务期间清理代码
    • 缺点
      • 代码味道
      • 性能不如访问者(没有双重分派,每次调用都投射)
      • 任务中的丑陋代码

我是不是漏掉了什么明显的东西?我熟悉许多四人组模式,以及 Game Programming Patterns 中的模式。如有任何帮助,我们将不胜感激。

明确地说,我不是在问 "best" 中的哪一个。我正在寻找这些方法的替代方法。

如果您正在寻找性能 - 访问者模式是必经之路。我个人甚至不会想出反射作为解决方案,因为它看起来不真实且过于复杂。转换有效,但在 OO 环境中,它通常被认为是 code smell 并且应该避免(例如使用访问者模式)。

另一个重要方面是可读性 vs 可写性:访问者模式在添加节点时可能需要更多工作并且需要更多维护,但它绝对更容易阅读,通常也更容易理解。反射在这两个方面都是行不通的,而转换又是代码味道。

我认为如果你不能有一个静态工厂class那么这是一个棘手的问题。如果允许使用静态工厂,那么这个简短的示例或许可以提供一些想法。

这种方法允许 运行-time 将 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

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

访客模式可以通过用户程序进行扩展。例如,给定访问者模式中的 classic 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 的简单映射来解决这个问题。我们可以为 bind/lookup 处理程序

发明一些 API
register(NodeX.class, TaskY.class, (x,y)->
{ 
    ...  
});

或匿名 class

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)

因此,在研究了 Java 8 个 Lambda 表达式以及如何以反射方式构建它们之后,我想到了从我获得的 Method 对象创建一个 BiConsumer 的想法反射性地,第一个参数是应该调用方法的实例,第二个参数是方法的实际参数:

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);

这种调用方法几乎完全消除了反射引起的调用开销,但它确实有一些 issues/constraints:

  • 由于使用了BiConsumer,方法只能传入一个参数(accept方法的第一个参数必须是调用该方法的实例) .
    • 这对我来说很好,反正我只想传递一个论点。
  • 调用具有以前从未见过的参数类型的方法时,性能开销不小,因为必须首先以反射方式搜索它。
    • 同样,就我的目的而言,这没问题;可接受的节点类型的数量不会很大,并且会在看到时快速缓存。在第一个 "discovery" 参数类型组合的适当方法之后,开销非常小(常数,我相信,因为它是一个简单的 HashMap 查找)。
  • 需要 Java 8(反正我已经用过了)

我可以通过使用自定义功能接口(类似于 Invoker class 而不是 Java 的 BiConsumer 来澄清这段代码,但是截至现在它以我想要的性能完全按照我的要求去做。