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

Cal*_*man 6 java reflection visitor

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

World
|--Zone
|----Cell
|------Actor
|------Actor
|--------Item
Run Code Online (Sandbox Code Playgroud)

如果a World可以包含多个Zone对象,则a Zone可以包含多个Cell对象,依此类推.

所有这些实现的Node接口,它有一些方法,如getParent,getChildren,update,reset等.

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

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

public void doTask(Actor node)
{
    if(!(node instanceof Goblin)) { return; }
    Goblin goblin = (Goblin) node;
}
Run Code Online (Sandbox Code Playgroud)

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

作为替代方案,我编写了一个实用类,它使用反射来查找适用于该方法的最具体方法Node.

我现在关心的是表现 ; 因为会有相当多的反思查找和调用,我担心我的游戏性能(每秒可能有数百或数千个这样的调用)会受到影响.

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

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

  1. 访客模式
    • 优点
      • 双重发货
      • 性能
      • 清理任务中的代码
    • 缺点
      • 很难添加新Node类型(不修改原始代码就不可能)
      • 在调用任务期间丑陋的代码
  2. 使用反射进行动态调用
    • 优点
      • 可以Node放弃添加新类型
      • 非常可定制的任务
      • 清理任务中的代码
    • 缺点
      • 表现不佳
      • 在调用任务期间丑陋的代码
  3. 铸件
    • 优点
      • 比反射更高效
      • 可能比访客更具活力
      • 在调用任务期间清理代码
    • 缺点
      • 代码味道
      • 性能低于访问者(没有双重调度,每次调用都要进行投射)
      • 任务中的丑陋代码

我错过了一些明显的东西吗?我熟悉许多四人组模式,以及游戏编程模式中的模式.任何帮助将在这里受到赞赏.

要清楚,我不是在问这些是哪个是"最好的".我正在寻找这些方法的替代方案.

Cal*_*man 4

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

consumer.accept(nodeTask, node);
Run Code Online (Sandbox Code Playgroud)

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

  • 由于使用了BiConsumer,因此只能将一个参数传递到该方法中(该accept方法的第一个参数必须是应调用该方法的实例)。
    • 这对于我的目的来说很好,无论如何我只想传递一个参数。
  • 当调用以前从未见过的参数类型的方法时,会产生不小的性能开销,因为必须首先进行反射性搜索。
    • 再说一遍,就我的目的而言,这是可以的;可接受的节点类型的数量不会很大,并且会在看到它们时快速缓存。在第一次“发现”参数类型组合的适当方法之后,开销非常小(我相信是恒定的,因为它是一个简单的HashMap查找)。
  • 需要 Java 8(反正我已经在使用了)

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