访客与被访者之间的沟通

时间:2013-03-05 09:40:51

标签: language-agnostic visitor

我当前的项目包含复杂的对象层次结构。以下结构是此层次结构的简化示例,用于演示目的:

  
      
  • 库   
        
    • 类别“小说”   
          
      • 类别“科幻小说”   
            
        • 预订A (每本书包含页面,此处未显示)
        •   
        • Book B
        •   
      •   
      • 类别“犯罪”   
            
        • Book C
        •   
      •   
    •   
    • 类别“非虚构”   
          
      • (许多子类别)
      •   
    •   
  •   

现在,每当我需要来自数据结构的一些信息时,我想避免在我的代码中嵌套循环,因为当结构发生变化时,我必须更新所有循环。

所以我打算使用访客模式,这似乎给了我所需的灵活性。它看起来像这样:

class Library
{
    void Accept(ILibraryVisitor visitor)
    {
        IterateCategories(this.categories, visitor);
    }

    void IterateCategories(
        IEnumerable<Category> categorySequence,
        ILibraryVisitor visitor)
    {
        foreach (var category in categorySequence)
        {
            visitor.VisitCategory(category.Name);

            IterateCategories(category.Subcategories, visitor);

            foreach (var book in category.Books)
            {
                // Could also pass in a book instance, not sure about that yet...
                visitor.VisitBook(book.Title, book.Author, book.PublishingDate);

                foreach (var page in book.Pages)
                {
                    visitor.VisitPage(page.Number, page.Content);
                }
            }
        }
    }
}

interface ILibraryVisitor
{
    void VisitCategory(string name);

    void VisitBook(string title, string author, DateTime publishingDate);

    void VisitPage(int pageNumber, string content);
}

我已经看到了一些可能的问题,所以我希望你能给我一些建议。

问题1

如果我想创建一个以其所属的(子)类别为前缀的书名列表(例如小说»科幻小说»书A ),一个简单的访客实现似乎会特技:

// LibraryVisitor is a base implementation with no-op methods
class BookListingVisitor : LibraryVisitor
{
    private Stack<string> categoryStack = new Stack<string>();

    void VisitCategory(string name)
    {
        this.categoryStack.Push(name);
    }

    // Other methods
}

这里我遇到了一个问题:我不清楚何时弹出堆栈,因为我不知道什么时候类别结束。将VisitCategory方法拆分为两种方法是常见的方法,如下所示?

interface ILibraryVisitor
{
    void VisitCategoryStart(string name);

    void VisitCategoryEnd();

    // Other methods
}

或者还有其他方法可以处理这样的结构,这些结构具有明确的开始和结束范围吗?

问题2

假设我只想列出1982年发布的书籍。装饰访问者会将过滤与列表逻辑分开:

class BooksPublishedIn1982 : LibraryVisitor
{
    private ILibraryVisitor visitor;

    public BooksPublishedIn1982(ILibraryVisitor visitor)
    {
        this.visitor = visitor;
    }

    void VisitBook(string title, string author, DateTime publishingDate)
    {
        if (publishingDate.Year == 1982)
        {
            this.visitor.VisitBook(string title, string author, publishingDate);
        }
    }

    // Other methods that simply delegate to this.visitor
}

这里的问题是仍然会在1982年没有发布的书籍中调用VisitPage。因此装饰者需要以某种方式与访问对象进行通信:

  

访客:'嘿,这本书不是1982年的,所以请不要告诉我任何关于它的事。'
    图书馆:'哦,好的,那我就不会向你展示它的页面了。'

访问方法目前返回void。我可以改变它以返回一个布尔值,它指示是否访问子项,但这感觉有点脏。是否存在让被访者知道应该跳过某些项目的常见做法?或许我应该研究一种不同的设计模式?

P.S。如果您认为这些应该是两个单独的问题,请告诉我,我会很乐意将它们分开。

1 个答案:

答案 0 :(得分:2)

GoF图书描述的访问者模式处理层次结构,而不处理对象层次结构。简单地说,添加一个新的访问者类型就像向基类和所有子项添加一个新的虚函数,而不需要触及他们的代码。

访问者的机制包括层次结构中每个类的一个Visitor::Visit函数,以及父类和所有后代中的Accept函数。它的工作原理是通过父类引用调用Accept(visitor)。在恰好被引用的对象中实现Accept会调用正确的Visitor::Visit(this)类型。它与我们根类的不同子类的实例之间可能存在的任何对象层次结构完全正交。

在您的情况下,ILibraryVisitor界面将采用VisitLibrary(Library)方法,VisitCategory(Category)方法,VisitBook(Book)方法等,而每个Library }},CategoryBook等将继承公共基类并重新实现其Accept(ILibraryVisitor)方法。

到目前为止一切顺利。但从这一点来看,你的实现似乎有点迷失方向。访客不会调用自己的访问功能!层次结构的成员,访问者为了他们的利益实现这些功能。那么我们如何进入类别树呢?

请注意,调用Accept(FooVisitor)会替换层次结构根目录中的方法Foo,而FooVisitor::VisitBar会替换bar::Foo的实现。当我们想要对某个对象做某事时,我们调用它的方法。不是吗?所以我们这样做(伪代码)。

class LibraryVisitor : ILibraryVisitor
{
  IterateChildren (List<ILibraryObject> objects) {
    foreach obj in objects {
      obj.Accept(this);
    }
  }
  IterateSubcategories (Category cat) {
    stack.push (cat);                 # we need a stack here to build a path
    IterateChildren (cat.children);   # both books and subcategories
    stack.pop();
  }
  VisitLibrary (Library) = abstract
  VisitCategory (Category) = abstract
  VisitBook (page) = abstract
  VisitPage (Page) = abstract
}

class MyLibraryVisitor : LibraryVisitor {
  VisitLibrary (Library l ) { ... IterateChildren (categories) ... }
  VisitCategory (Category c) = { ... IterateSubcategories (c) ... }
  VisitBook (Book) = { ... IterateChildren (pages) ... }
  VisitPage (Page) = { ... no children here, end of walk ... }
}

请注意VisitAccept之间的乒乓操作。 Visitor对当前被访者的子女发出Accept次电话,孩子们回复Visitor::Visit,并且访客在他们的孩子等上致电Accept < / p>

这就是你的第二个问题的回答:

class BooksPublishedIn1982:LibraryVisitor    {      VisitBook(Book b){        如果b.publishedIn(1982){          IterateChildren(b.pages)        }      }    }

很明显,树木步行和游客机器几乎没有任何关系。

我已决定使用每个Visit实现完全迭代或不迭代子项。不一定是这种情况,您可以轻松地将每个VisitXYZ拆分为两个函数VisitXYZProperVisitXYZChildren。默认情况下,VisitXYZ将同时调用这两个访问者,每个具体访问者都可以覆盖该决定。