4

介绍

显然,我的整个程序员生涯都在做一个“非正统”的访问者模式。

是的,我从访问者的方法中分派到一个具体的复合元素访问Visit方法。

我想这就是我学习它的方式,但现在我找不到任何例子,我学习它的来源也不见了。

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

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

例子

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

// "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);
}

现在,为了防守...

优势一

假设我们要添加一个新的 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声明)。

优势二

假设我们想在一个单独的包中扩展组合。我们可以在不修改的情况下适应这一点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 { }

拥有这种结构使我们能够打破为了容纳扩展的 CompositeElement 类型而BaseVisitor需要拥有的依赖关系。VisitExtended

结论

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

请权衡您对不利因素的看法。

4

5 回答 5

12

GoF 书中按原样定义访问者模式的主要原因是 C++ 没有任何形式的运行时类型标识 (RTTI)。他们使用“双重调度”来让目标对象告诉他们他们的类型是什么。一个很酷,但很难描述的把戏。

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

GoF 访问者模式使用数据对象本身来执行分派,方法是提供一个“接受”方法,该方法转身并将“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);
}

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

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

希望这可以帮助!

于 2010-11-05T20:21:02.787 回答
4

看看非循环访问者模式。它还提供了您在访问者改编中列出的优势,没有大的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) { }
}

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

于 2010-11-05T21:03:49.733 回答
2

除了您已经提到的缺点(性能和需要维护大的 switch 语句)之外,另一个问题是使用 GoF 访问者模式,添加 CompositeElement 的新子类将迫使您为其编写处理程序,否则您的代码将不会t 甚至编译。另一方面,使用您的方法,很容易添加新的 CompositeElement 子类而忘记更新适当的访问者 switch 语句。

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

于 2010-11-05T20:31:11.787 回答
1

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

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

于 2010-11-05T20:37:00.370 回答
0

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

请看一下我写的一篇关于这个的文章

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

于 2011-01-29T23:37:18.160 回答