访客模式的变化:为什么不将第二次调度移动到访问者的“访问”方法中?

时间:2010-11-05 19:35:03

标签: design-patterns visitor-pattern

简介

显然,我在整个程序员生活中一直在做一个“非正统的”访客模式。

是的,我从访问者的Visit方法发送到具体的复合元素访问方法。

我认为这就是我学习它的方式,但是现在我找不到它的任何例子,我从中学到的来源已经消失了。

现在,面对压倒性的证据表明具体的元素调度进入复合元素的Accept方法,我想知道我这样做的方式至少有一些优势。我认为的两个优点是:

  1. 我有一个地方可以决定如何派遣:基地访客。
  2. 我可以添加新的复合元素类型,并让基本访问者忽略它们,但派生的访问者可以覆盖Visit来处理它们。
  3. 实施例

    以下是基本的复合/访客模型:

    // "Unorthodox" version
    public class BaseVisitor 
    {
        public virtual void Visit(CompositeElement e)
        {
             if(e is Foo)
             {
                 VisitFoo((Foo)e);
             }
             else if(e is Bar)
             {             
                 VisitBar((Bar)e);
             }
             else
             {
                 VisitUnknown(e);
             }
        }
    
        protected virtual void VisitFoo(Foo foo) { }
        protected virtual void VisitBar(Bar bar) { }
        protected virtual void VisitUnknown(CompositeElement e) { }
    } 
    
    public class CompositeElement 
    {
        public virtual void Accept(BaseVisitor visitor) { } 
    }
    
    public class Foo : CompositeElement { }
    public class Bar : CompositeElement { }
    

    请注意,访问者类现在负责第二个基于类型的调度,而不是规范版本,例如,Foo将负责它并且具有:

    // Canonical visitor pattern 2nd dispatch
    public override void Accept(BaseVisitor visitor)
    {
        visitor.VisitFoo(this);
    }
    

    现在,为了辩护...

    优势1

    假设我们要添加一个新的CompositeElement类型:

    public class Baz : CompositeElement { }
    

    为了在访问者模型中容纳这种新元素类型,我只需要对BaseVisitor类进行更改:

    public class BaseVisitor 
    {  
        public virtual void Visit(CompositeElement e)
        {
            // Existing cases elided...
            else if(e is Baz)
            {
                VisitBaz((Baz)e);
            }
        }
    
        protected virtual void VisitBaz(Foo foo) { }
    }
    

    不可否认,这是一个小问题,但它似乎可以简化维护(即,如果您不介意大ifswitch语句)。

    优势2

    假设我们想要在单独的包中扩展复合。我们可以在不修改BaseVisitor

    的情况下容纳这一点
    public class ExtendedVisitor : BaseVisitor
    {
        public override Visit(CompositeElement e)
        {
            if(e is ExtendedElement)
            {
                VisitExtended((ExtendedElement)e);
            }
            else
            {
                base.Visit(e);
            }            
        }
    
        protected virtual void VisitExtended(ExtendedElement e) { }
    }
    
    public class ExtendedCompositeElement : CompositeElement { }
    

    拥有这种结构允许我们打破BaseVisitor需要拥有VisitExtended的依赖关系,以便容纳扩展的CompositeElement类型。

    结论

    我没有足够实施访客模式或维持足够长的时间,以至于在这一点上有任何不利因素。显然,维护一个大的switch语句是一种痛苦,并且存在性能影响,但是我不确定它们是否超过保持BaseVisitor不依赖于扩展的灵活性。

    请考虑你对缺点的看法。

5 个答案:

答案 0 :(得分:11)

访问者模式在GoF书籍中定义的主要原因是C ++没有任何形式的运行时类型识别(RTTI)。他们使用“双重调度”来获取目标对象,告诉他们他们的类型是什么。非常酷,但难以描述的技巧。

您描述的内容与GoF访问者模式(正如您所提到的)之间的主要区别在于您有一个明确的“调度”方法 - “访问”方法,它检查参数的类型并将其发送到显式的visitFoo ,visitBar等方法。

GoF Visitor模式使用数据对象本身来执行调度,方法是提供一个“accept”方法,该方法可以转换并将“this”传递回访问者,并解析为正确的方法。

要把它放在一个地方,基本的GoF模式看起来像(我是一个Java人,所以请原谅Java代码而不是C#)

public interface Visitor {
    void visit(Type1 value1);
    void visit(Type2 value2);
    void visit(Type3 value3);
}

(请注意,如果您愿意,此接口可以是具有默认方法实现的基类)

并且您的数据对象所有需要实现“接受”方法:

public class Type1 {
    public void accept(Visitor v) {
        v.visit(this);
    }
}

注意:这与你提到的GoF版本之间的最大区别在于我们可以使用方法重载,因此“访问”方法名称保持一致。这允许每个数据对象具有相同的“接受”实现,从而减少拼写错误的可能性

每种类型都需要完全相同的方法代码。 accept方法中的“this”会导致编译器解析为正确的访问方法。

然后,您可以根据需要实现访问者界面。

请注意,在相同或不同的包中添加新类型(例如Type4)将需要的更改少于您描述的更改。如果在同一个包中,我们会向Visitor接口(以及每个实现)添加一个方法,但是您不需要“dispatch”方法。

那说......

  • GoF实现需要合作/修改数据对象。这是我不喜欢它的主要事情(除了试图向某人描述它,这可能是非常痛苦的。很多人在“双重调度”概念上遇到麻烦)。我非常喜欢保留我的数据以及我将要用它做什么 - MVC类型方法。
  • 您的实现和GoF实现都需要更改代码才能添加新类型 - 这可能会破坏现有的访问者实现
  • 您的实现和GoF实现都是静态的;特定类型的“做什么”不能在运行时更改
  • 我们现在使用最常用的语言RTTI

顺便说一句,我在约翰霍普金斯大学教授设计模式,我想推荐的是一种非常动态的方法。

从一个更简单的单对象访问者界面开始:

public interface Visitor<T> {
    void visit(T type);
}

然后创建一个VisitorRegistry

public class VisitorRegistry {
    private Map<Class<?>, Visitor<?>> visitors = new HashMap<Class<?>, Visitor<?>>();
    public <T> void register(Class<T> clazz, Visitor<T> visitor) {
        visitors.put(clazz, visitor);
    }
    public <T> void visit(T thing) {
        // needs error checks, and possibly "walk up" to check supertypes if direct type not found
        // also -- can provide default action to perform - maybe register using Void.class?
        @SuppressWarnings("unchecked")
        Visitor<T> visitor = (Visitor<T>) visitors.get(thing.getClass());
        visitor.visit(thing);
    }
}

你会像

一样使用它
VisitorRegistry registry = new VisitorRegistry();
registry.register(Person.class, new Visitor<Person>() {
    @Override public void visit(Person person) {
        System.out.println("I see " + person.getName());
    }});
// register other types similarly

// walk the data however you would...
for (Object thing : things) {
    registry.visit(thing);
}

这使您现在可以为要访问的每种类型注册独立访问者,并且无论何时添加新类型,它都不会破坏现有的访问者实施。

您还可以在运行时重新注册(和取消注册)访问者的不同组合,甚至可以从某些配置信息中加载定义。

希望这有帮助!

答案 1 :(得分:3)

看看acyclic visitor pattern。它还提供了您在访客改编中列出的优势,没有大switch声明:

// acyclic version 
public interface IBaseVisitor { }
public interface IBaseVisitor<T> : IBaseVisitor where T : CompositeElement {
  void Visit(T e) { }
}
public class CompositeElement {
  public virtual void Accept(IBaseVisitor visitor) { }
}
public class Foo : CompositeElement {
  public override void Accept(IBaseVisitor visitor) {
    if (visitor is IBaseVisitor<Foo>) {
      ((IBaseVisitor<Foo>)visitor).Visit(this);
    }
  }
}
public class Bar : CompositeElement {
  public override void Accept(IBaseVisitor visitor) {
    if (visitor is IBaseVisitor<Bar>) {
      ((IBaseVisitor<Bar>)visitor).Visit(this);
    }
  }
}

您的真实访问者可以选择访问哪些子类:

public class MyVisitor : IBaseVisitor<Foo>, IBaseVisitor<Bar> {
  public void Visit(Foo e) { }
  public void Visit(Bar e) { }
}

它是“非循环的”,因为它在层次结构中的类型与访问者中的方法之间没有循环依赖关系。

答案 2 :(得分:2)

除了你已经提到的缺点(性能和需要维护一个大的switch语句),另一个问题是使用GoF Visitor模式,添加一个新的CompositeElement子类会强制你为它或你的代码编写一个处理程序代码甚至不会编译。另一方面,使用您的方法,添加新的CompositeElement子类并忘记更新相应的访问者switch语句将很容易。

您建议对访问者进行子类化,仅处理某些访问者中的一部分类,这会使情况更糟。现在,当开发人员创建CompositeElement的新子类时,他们需要熟悉所有现有的访问者类,以便知道哪些访问者做了哪些以及哪些不需要更改,这很容易出错。

答案 3 :(得分:1)

某些语言也有限制,使其非常缺乏吸引力。除了通过接口之外,Java没有多重继承。要求每个复合元素和访问者从相同的基类派生,这将构成一个粗略的类型层次结构。

即。你的方式不允许Visitor和CompositeElement成为接口。

答案 4 :(得分:0)

我不喜欢使用visitA,visitB,visitWhatever,acceptA,acceptB,​​acceptWhatever的实现,因为这种方法意味着每次向层次结构添加类时都会破坏接口。

请查看an article I've written about this

文章详细解释了这一点,使用现实生活中的例子,包括一个不破坏任何界面的多态案例。