1

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

  • 图书馆
    • “小说”类别
      • 分类“科幻”
        • 书A (每本书包含页数,此处不显示)
        • 书 B
      • 类别“犯罪”
        • 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

如果我想创建一个以它所属的(子)类别为前缀的书名列表(例如Fiction » Science Fiction » Book 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。我可以将其更改为返回一个布尔值,指示是否访问子项目,但这感觉有点脏。是否有让访问者知道它应该跳过某些项目的常见做法?或者也许我应该研究一种不同的设计模式?

PS如果您认为这应该是两个独立的问题,请告诉我,我很乐意将它们分开。

4

1 回答 1

3

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

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

在您的情况下,ILibraryVisitor接口将具有一个VisitLibrary(Library)方法、一个VisitCategory(Category)方法、一个方法等,VisitBook(Book)而 、 等中的每一个都Library将继承一个公共基类并重新实现其方法。CategoryBookAccept(ILibraryVisitor)

到现在为止还挺好。但是从这一点开始,您的实现似乎有点迷失方向。访问者不调用自己的访问函数!层次结构的成员这样做,Visitor 实现这些功能是为了他们的利益。那么我们如何去分类树呢?

请记住,调用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 ... }
}

Visit注意和之间的乒乓动作AcceptVisitor拜访Accept当前访问者的孩子,孩子们Visitor::Visit回电,访客拜访他们的Accept孩子等。

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

类 BooksPublishedIn1982 : LibraryVisitor { VisitBook (Book b) { if b.publishedIn (1982) { IterateChildren(b.pages) } } }

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

Visit我已经决定在每个实现中完全迭代或不迭代子级。不必如此,您可以轻松地将每个函数拆分VisitXYZ为两个函数,VisitXYZProper并且VisitXYZChildren. 默认情况下,VisitXYZ将调用两者,并且每个具体访问者都可以覆盖该决定。

于 2013-03-05T19:26:10.070 回答