在运行时动态选择方法;访问者模式或反射的替代方案
Dynamically choose method at runtime; alternatives to Visitor Pattern or Reflection
我正在制作一个小型游戏模板,其中的世界由如下节点组成:
World
|--Zone
|----Cell
|------Actor
|------Actor
|--------Item
一个World
可以包含多个Zone
对象,一个Zone
可以包含多个Cell
对象,依此类推。
其中每一个都实现了 Node
接口,它有一些方法,如 getParent
、getChildren
、update
、reset
等。
我希望能够在单个节点上执行给定的 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,这就是我来这里的原因):
- 访客模式
- 优点
- 双重派遣
- 性能
- 清理任务中的代码
- 缺点
- 难以添加新的
Node
类型(不修改原始代码是不可能的)
- 调用任务期间的丑陋代码
- 使用反射的动态调用
- 优点
- 可以使用 abandon
添加新的 Node
类型
- 高度可定制的任务
- 清理任务中的代码
- 缺点
- 表现不佳
- 调用任务期间的丑陋代码
- 铸造
- 优点
- 比反射更高效
- 可能比访客更动态
- 调用任务期间清理代码
- 缺点
- 代码味道
- 性能不如访问者(没有双重分派,每次调用都投射)
- 任务中的丑陋代码
我是不是漏掉了什么明显的东西?我熟悉许多四人组模式,以及 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 Node
和 Visitor
定义,用户可以定义
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
来澄清这段代码,但是截至现在它以我想要的性能完全按照我的要求去做。
我正在制作一个小型游戏模板,其中的世界由如下节点组成:
World
|--Zone
|----Cell
|------Actor
|------Actor
|--------Item
一个World
可以包含多个Zone
对象,一个Zone
可以包含多个Cell
对象,依此类推。
其中每一个都实现了 Node
接口,它有一些方法,如 getParent
、getChildren
、update
、reset
等。
我希望能够在单个节点上执行给定的 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,这就是我来这里的原因):
- 访客模式
- 优点
- 双重派遣
- 性能
- 清理任务中的代码
- 缺点
- 难以添加新的
Node
类型(不修改原始代码是不可能的) - 调用任务期间的丑陋代码
- 难以添加新的
- 优点
- 使用反射的动态调用
- 优点
- 可以使用 abandon 添加新的
- 高度可定制的任务
- 清理任务中的代码
Node
类型 - 缺点
- 表现不佳
- 调用任务期间的丑陋代码
- 优点
- 铸造
- 优点
- 比反射更高效
- 可能比访客更动态
- 调用任务期间清理代码
- 缺点
- 代码味道
- 性能不如访问者(没有双重分派,每次调用都投射)
- 任务中的丑陋代码
- 优点
我是不是漏掉了什么明显的东西?我熟悉许多四人组模式,以及 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 Node
和 Visitor
定义,用户可以定义
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 处理程序
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
查找)。
- 同样,就我的目的而言,这没问题;可接受的节点类型的数量不会很大,并且会在看到时快速缓存。在第一个 "discovery" 参数类型组合的适当方法之后,开销非常小(常数,我相信,因为它是一个简单的
- 需要 Java 8(反正我已经用过了)
我可以通过使用自定义功能接口(类似于 Invoker
class 而不是 Java 的 BiConsumer
来澄清这段代码,但是截至现在它以我想要的性能完全按照我的要求去做。