1

現在のプロジェクトには、複雑なオブジェクト階層が含まれています。次の構造は、デモンストレーション目的でこの階層を単純化した例です。

  • としょうかん
    • カテゴリ「フィクション」
      • カテゴリ「SF」
        • ブック 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 メソッドを 2 つのメソッドに分割するのは一般的なアプローチですか?

interface ILibraryVisitor
{
    void VisitCategoryStart(string name);

    void VisitCategoryEnd();

    // Other methods
}

それとも、このような構造を扱う他の方法があり、開始と終了の明確な範囲がありますか?

質問2

1982 年に出版された本だけを一覧表示したいとします。decorator ビジターは、フィルタリングを一覧表示ロジックから分離します。

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
}

ここでの問題は、VisitPage が 1982 年に出版されていない本に対して呼び出されることです。そのため、デコレーターは何らかの形で訪問済みオブジェクトと通信する必要があります。

訪問者: 「この本は 1982 年のものではないので、何も言わないでください。」
図書館: 「わかりました。では、そのページは表示しません。」

現在、訪問メソッドは void を返します。サブアイテムにアクセスするかどうかを示すブール値を返すように変更することもできますが、それはちょっと汚い感じがします。特定の項目をスキップする必要があることを訪問者に知らせる一般的な方法はありますか? それとも、別の設計パターンを検討する必要がありますか?

PS これらが 2 つの別個の質問であると思われる場合は、お知らせください。喜んで分割させていただきます。

4

1 に答える 1

3

GoF ブックで説明されている Visitor パターンは、オブジェクト階層ではなく、クラス階層を扱います。簡単に言うと、新しい Visitor 型を追加することは、コードに触れることなく、基本クラスとすべての子クラスに新しい仮想関数を追加するのと同じように機能します。

Visitor::VisitVisitor の機構は、階層内のクラスごとに 1 つの関数Acceptと、親クラスおよびすべての子孫の関数で構成されます。Accept(visitor)親クラス参照を介して呼び出すことで機能します。Acceptたまたま参照されるオブジェクト内の の実装は、適切な種類の を呼び出しますVisitor::Visit(this)。これは、ルート クラスの異なるサブクラスのインスタンス間に存在するオブジェクト階層と完全に直交しています。

あなたの場合、ILibraryVisitorインターフェイスにはVisitLibrary(Library)メソッド、VisitCategory(Category)メソッド、メソッドなどがありますが、、などVisitBook(Book)はそれぞれ共通の基本クラスを継承し、そのメソッドを再実装します。LibraryCategoryBookAccept(ILibraryVisitor)

ここまでは順調ですね。しかし、この時点から、実装は少し混乱しているようです。Visitor はそれ自身の Visit 関数を呼び出しません! 階層のメンバーはそうします。Visitor は、これらの関数を利益のために実装します。では、カテゴリ ツリーをどのように下に移動するのでしょうか。

を呼び出すと、階層のルートにあるAccept(FooVisitor)メソッドが置き換えられ、 の実装が置き換えられることに注意してください。オブジェクトで何かをしたいときは、そのメソッドを呼び出します。そうじゃない?では、(疑似コードで) やってみましょう。FooFooVisitor::VisitBarbar::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との間のピンポン動作に注意してくださいAccept。現在の訪問者の子供たちを呼び出し、子供たちはコールVisitorバックし、訪問者は自分子供たちを呼び出します。AcceptVisitor::VisitAccept

2 番目の質問に対する回答は次のとおりです。

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

繰り返しになりますが、ツリー ウォークと訪問者の機械は、互いにほとんど何の関係もないことが明らかです。

Visit各実装で子を完全に反復するか反復しないかの決定は残しました。そうである必要はありません。それぞれVisitXYZを 2 つの関数にVisitXYZProper簡単に分割できますVisitXYZChildren。デフォルトでは、VisitXYZは両方を呼び出し、各具体的なビジターはその決定をオーバーライドできます。

于 2013-03-05T19:26:10.070 に答える