代码之家  ›  专栏  ›  技术社区  ›  codekaizen

访问者模式的一个变体:为什么不将第二个调度移到访问者的“Visit”方法中?

  •  3
  • codekaizen  · 技术社区  · 15 年前

    简介

    是的,我从访问者的 Visit 方法。

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

    Accept 方法,我想知道我的方法是否至少有一些优势。在我看来有两个优点:

    1. 我只有一个地方决定如何派遣:基地访客。
    2. 我可以添加新的复合元素类型,并让基础访问者忽略它们,但是派生访问者可以重写 访问

    实例

    下面是基本的复合/访问者模型:

    // "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

    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) { }
    }
    

    诚然,这是一个小问题,但它看起来确实简化了维护(也就是说,如果您不介意 if 或 switch

    优势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 { }
    

    基地访客 需要拥有 VisitExtended 以便容纳扩展的复合元素类型。

    我没有实现足够的访问者模式,也没有维护足够长的时间,以至于在这一点上有任何不利之处。显然,保持一个大的转换语句是一种痛苦,也有性能的影响,但是我不确定它们是否超过保持灵活性的灵活性。 基地访客

    请考虑一下你的缺点。

    5 回复  |  直到 15 年前
        1
  •  11
  •   Scott Stanchfield    15 年前

    在GoF书中定义访问者模式的主要原因是,C++没有任何形式的运行时类型标识(RTTI)。他们使用“双重分派”让目标对象告诉他们他们的类型。一个相当酷,但难以形容的把戏。

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

    把这一切放在一个地方,基本的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版本中提到的最大区别是,我们可以使用方法重载,以便“visit”方法名保持一致。这使得每个数据对象都有一个相同的“accept”实现,减少了出错的机会

    然后,您可以根据自己的需要实现Visitor接口。

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

    • GoF实现需要数据对象的协作/修改。这是我最不喜欢的事情(除了试图向某人描述它,这可能会很痛苦)。很多人对“双重派遣”的概念有异议。我非常喜欢把我的数据和我要用它做什么分开-MVC类型的方法。
    • 您的实现和GoF实现都是静态的;不能在运行时更改特定类型的“要做什么”

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

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

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

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

    希望这有帮助!

        2
  •  3
  •   Jordão    13 年前

    看看 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) { }
    }
    

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

        3
  •  2
  •   Luke Hutteman    15 年前

    你建议对访问者进行子类化,在某些访问者中只处理类的子集,这会使情况更糟。现在,当开发人员创建CypItEngEnter的新子类时,他们将需要对现有的所有访问者类进行密切的了解,以便知道哪些访问者类和哪些类不需要更改,这将很容易出错。

        4
  •  1
  •   Mark Peters    15 年前

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

    i、 你的方法不允许访问者和CompositeElement成为接口。

        5
  •  0
  •   Richard Gomes    13 年前

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

    请看一下 an article I've written about this .

    本文使用真实的例子详细地解释了这一切,包括一个不破坏任何接口的多态案例。

    推荐文章