在已知类型的地方跳过 "accept",这是对访问者模式的有效优化吗?

Is skipping "accept" where type is known, a valid optimization for the Visitor pattern?

考虑以下访问者的简单语言解释器。

public interface Visitor{
    void visit( VarStat vs);
    void visit( Ident i);
    void visit( IntLiteral a);
    void visit( Sum s);
}

为了完整起见,我添加了一些提供必要实现细节的代码(您可以跳过并直接阅读问题)。

public interface Visitable{
    void accept( Visitor v);
}

public class VarStat implements Visitable{
    Ident i;
    Exp   e;

    public VarStat(Ident id, Exp ex){
        i = id;
        e = ex;
    }

    public Ident getIdent() { return i; }
    public Exp getExp() { return e; }

    @Override
    public void accept( Visitor v){
        v.visit( this);
    }
}

public interface Exp extends Visitable{

}

public class Ident implements Exp{
    @Override
    public void accept( Visitor v){
        v.visit( this);
    }
}

一个var语句是这样定义的:

VarStat ::== var Ident = Exp;
Exp ::== Exp + Exp | IntLiteral | Ident
IntLiteral ::== [0-9]{0,8}
Ident ::== [a-zA-Z]+

一个有效的语言实例

var x = x+y+4;

表示 VarStat 节点的抽象方式如下:

.               _____VarStat _____
.              /       /  | \     \ 
.             /       /   |  \     \  
.            /       /    |   \     \
.         "var"   Ident  "="  Exp   ";"

问题

通常的 VisitorPattern 应用程序是

void visit( VarStat vs){
     vs.getIdent().accept( this);
     vs.getExp().accept( this);
     //...
}

但是,因为我知道 "Ident" 是 Ident 类型,所以可能的优化是

void visit( VarStat vs){

     visit( vs.getIdent());
     vs.getExp().accept( this);
     //...
}

这将跳过 2 个提高性能的方法调用(实际上它在我的场景中提供了很好的提升)。

这是否被认为是可能导致未来问题的设计错误?

它会引起问题吗?很难说。 (我想如果语法改变了可能会引起意外...)

但我认为真正的问题是这是否是一个值得的优化。具体来说,保存两个方法调用是否会对全局产生重大影响?我的直觉是不会。

这个解释器的性能真的很重要吗?

如果是,为什么要在解释器中使用访问者模式?你不应该编译成中间虚拟机代码吗?还是字节码?

在这种情况下,它不会是访客模式。也不一定符合你的要求,访问者经常被误用,导致过度架构。

但是,您将失去潜在的好处。例如,在将调用转发到 decorated/proxied 对象之前,您将无法为 Ident 创建装饰器或代理并在 accept 方法中执行其他操作。

Visitor 只是一个复杂的脚手架,用于在 Java 等语言上实现双重调度。

当你处理叶子类型时,你不需要双重调度;运行时类型在编译时已知。直接dispatch一个leaf type不仅是一种优化,更不符合原则。

当然,问题是,将来叶子类型可能会变成超类型。使用当今 IDE 中的重构工具,这不是一个大问题。

为现在的需求做简单的设计,不如为未知的未来需求做复杂的设计。


在java8中,我们可以使用非常接近真正的双分派的语法来实现双分派

final DoubleDispatch<Root,Void> dd = new DoubleDispatch<>();

dd.register(X.class, x->
{
    do something with x; its compile time type is X
    return null;
});
dd.register(Y.class, y->
{
    do something with y; its compile time type is Y
    return null;
});
// etc

...
dd.invoke( something );



// ----

public class DoubleDispatch<T, R>
{
    public R invoke(T obj){...}

    public <C extends T> void register(Class<C> type, Function<C,R> func){...}
}

另见 - Java Class.cast() and Overload