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

cod*_*zen 3 design-patterns visitor-pattern

介绍

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

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

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

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

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

例子

这是基本的Composite/Visitor模型:

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

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

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

现在,为防守......

优势1

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

public class Baz : CompositeElement { }
Run Code Online (Sandbox Code Playgroud)

为了在访问者模型中容纳这个新元素类型,我只需要对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) { }
}
Run Code Online (Sandbox Code Playgroud)

不可否认,这是一个小问题,但它确实似乎简化了维护(也就是说,如果你不介意大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 { }
Run Code Online (Sandbox Code Playgroud)

具有这种结构使我们能够打破的依赖BaseVisitor需要有VisitExtended为了适应扩大CompositeElement对象类型.

结论

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

请考虑一下您对缺点的看法.

Sco*_*eld 11

访问者模式在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);
}
Run Code Online (Sandbox Code Playgroud)

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

并且您的数据对象需要实现"接受"方法:

public class Type1 {
    public void accept(Visitor v) {
        v.visit(this);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

那说......

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

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

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

public interface Visitor<T> {
    void visit(T type);
}
Run Code Online (Sandbox Code Playgroud)

然后创建一个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);
    }
}
Run Code Online (Sandbox Code Playgroud)

你会喜欢这样的

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

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

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

希望这可以帮助!

  • 我认为每当添加新类型时打破现有的访问者实现是GoF模式的优势,而不是缺点.破坏使开发人员能够立即反馈需要以某种方式处理的案例; 然后,他们可以选择是否使用新类的适当逻辑来处理它,或者可能通过抛出IllegalStateException以防该类不适用于正在更新的访问者. (4认同)

Jor*_*dão 5

看一下非循环访问者模式。它还提供了您在访客适应中列出的优点,但没有大的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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

您的真正访问者可以选择他们访问的子类:

public class MyVisitor : IBaseVisitor<Foo>, IBaseVisitor<Bar> {
  public void Visit(Foo e) { }
  public void Visit(Bar e) { }
}
Run Code Online (Sandbox Code Playgroud)

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