95

訪問者パターンとその用途について本当に混乱しています。このパターンを使用する利点やその目的を視覚化できないようです。可能であれば誰かが例を挙げて説明できれば、それは素晴らしいことです。

4

6 に答える 6

214

ですから、ビジター パターンに関する無数の異なる説明を読んだことがあるかもしれません。

伝統的に、ビジターは型安全性を犠牲にすることなく型テストを実装するために使用されてきました。次のようないくつかのクラスがあるとします。

abstract class Fruit { }
class Orange : Fruit { }
class Apple : Fruit { }
class Banana : Fruit { }

そして、次を作成するとしましょうFruit[]:

var fruits = new Fruit[]
    { new Orange(), new Apple(), new Banana(),
      new Banana(), new Banana(), new Orange() };

リストを 3 つのリストに分割し、それぞれにオレンジ、リンゴ、またはバナナを含めたいと考えています。どのようにしますか?簡単解決策は、型テストです。

List<Orange> oranges = new List<Orange>();
List<Apple> apples = new List<Apple>();
List<Banana> bananas = new List<Banana>();
foreach (Fruit fruit in fruits)
{
    if (fruit is Orange)
        oranges.Add((Orange)fruit);
    else if (fruit is Apple)
        apples.Add((Apple)fruit);
    else if (fruit is Banana)
        bananas.Add((Banana)fruit);
}

動作しますが、このコードには多くの問題があります:

  • まず、醜いです。
  • 型安全ではないため、実行時まで型エラーをキャッチしません。
  • 維持できません。Fruit の新しい派生インスタンスを追加する場合、果物の型テストを実行するすべての場所に対してグローバル検索を行う必要があります。そうしないと、型を見逃す可能性があります。

訪問者パターンは問題をエレガントに解決します。基本の Fruit クラスを変更することから始めます。

interface IFruitVisitor
{
    void Visit(Orange fruit);
    void Visit(Apple fruit);
    void Visit(Banana fruit);
}

abstract class Fruit { public abstract void Accept(IFruitVisitor visitor); }
class Orange : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Apple : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Banana : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }

コードをコピーして貼り付けているように見えますが、派生クラスはすべて異なるオーバーロード (Apple呼び出しVisit(Apple)Banana呼び出しなど) を呼び出していることに注意してくださいVisit(Banana)

ビジターを実装します。

class FruitPartitioner : IFruitVisitor
{
    public List<Orange> Oranges { get; private set; }
    public List<Apple> Apples { get; private set; }
    public List<Banana> Bananas { get; private set; }

    public FruitPartitioner()
    {
        Oranges = new List<Orange>();
        Apples = new List<Apple>();
        Bananas = new List<Banana>();
    }

    public void Visit(Orange fruit) { Oranges.Add(fruit); }
    public void Visit(Apple fruit) { Apples.Add(fruit); }
    public void Visit(Banana fruit) { Bananas.Add(fruit); }
}

これで、型テストなしで果物を分割できます。

FruitPartitioner partitioner = new FruitPartitioner();
foreach (Fruit fruit in fruits)
{
    fruit.Accept(partitioner);
}
Console.WriteLine("Oranges.Count: {0}", partitioner.Oranges.Count);
Console.WriteLine("Apples.Count: {0}", partitioner.Apples.Count);
Console.WriteLine("Bananas.Count: {0}", partitioner.Bananas.Count);

これには次の利点があります。

  • 比較的きれいで、コードが読みやすい。
  • タイプ セーフ、タイプ エラーはコンパイル時にキャッチされます。
  • 保守性。具体的な Fruit クラスを追加または削除すると、それに応じて型を処理するように IFruitVisitor インターフェイスを変更できます。コンパイラはインターフェイスを実装するすべての場所をすぐに見つけて、適切な変更を加えることができます。

そうは言っても、ビジターは通常過剰であり、API を非常に複雑にする傾向があり、新しい種類の動作ごとに新しいビジターを定義するのは非常に面倒です。

通常、訪問者の代わりに継承のような単純なパターンを使用する必要があります。たとえば、原則として、次のようなクラスを作成できます。

class FruitPricer : IFruitVisitor
{
    public double Price { get; private set; }
    public void Visit(Orange fruit) { Price = 0.69; }
    public void Visit(Apple fruit) { Price = 0.89; }
    public void Visit(Banana fruit) { Price = 1.11; }
}

それは機能しますが、この簡単な変更よりも優れている点は次のとおりです。

abstract class Fruit
{
    public abstract void Accept(IFruitVisitor visitor);
    public abstract double Price { get; }
}

したがって、次の条件が当てはまる場合は訪問者を使用する必要があります。

  • 訪問される明確に定義された既知のクラスのセットがあります。

  • 上記のクラスに対する操作は、明確に定義されていないか、事前に知られていません。たとえば、誰かがあなたの API を使用していて、新しいアドホック機能をオブジェクトに追加する方法をコンシューマーに提供したい場合などです。また、アドホック機能を使用してシール クラスを拡張する便利な方法でもあります。

  • オブジェクトのクラスの操作を実行し、実行時の型テストを避けたい場合。これは通常、異なるプロパティを持つ異なるオブジェクトの階層をトラバースする場合に当てはまります。

次の場合は訪問者を使用しないでください。

  • 派生型が事前にわかっていないオブジェクトのクラスに対する操作をサポートします。

  • オブジェクトに対する操作は、特に基本クラスから継承できる場合やインターフェイスで定義できる場合は、事前に明確に定義されています。

  • クライアントは、継承を使用してクラスに新しい機能を簡単に追加できます。

  • 同じプロパティまたはインターフェイスを持つオブジェクトの階層をトラバースしています。

  • 比較的単純な API が必要です。

于 2010-04-09T02:41:40.593 に答える
78

昔々...

class MusicLibrary {
    private Set<Music> collection ...
    public Set<Music> getPopMusic() { ... }
    public Set<Music> getRockMusic() { ... }
    public Set<Music> getElectronicaMusic() { ... }
}

次に、ライブラリのコレクションを他のジャンルでフィルタリングできるようにしたいと考えています。新しい getter メソッドを追加し続けることができます。または、ビジターを使用することもできます。

interface Visitor<T> {
    visit(Set<T> items);
}

interface MusicVisitor extends Visitor<Music>;

class MusicLibrary {
    private Set<Music> collection ...
    public void accept(MusicVisitor visitor) {
       visitor.visit( this.collection );
    }
}

class RockMusicVisitor implements MusicVisitor {
    private final Set<Music> picks = ...
    public visit(Set<Music> items) { ... }
    public Set<Music> getRockMusic() { return this.picks; }
}
class AmbientMusicVisitor implements MusicVisitor {
    private final Set<Music> picks = ...
    public visit(Set<Music> items) { ... }
    public Set<Music> getAmbientMusic() { return this.picks; }
}

アルゴリズムからデータを分離します。アルゴリズムを訪問者の実装にオフロードします。データを保持するクラスを常に変更 (および肥大化) するのではなく、より多くのビジターを作成して機能を追加します。

于 2010-04-09T00:11:13.543 に答える
6

抽象化の別のレイヤーを提供します。オブジェクトの複雑さを軽減し、よりモジュール化します。インターフェイスを使用するようなものです(実装は完全に独立しており、それがどのように行われるかは誰も気にしません。)

現在、私はそれを使用したことはありませんが、次の場合に役立ちます。別のクラスがすべての関数を実装するさまざまな方法でサブクラスを実装する必要があるため、異なるサブクラスで実行する必要がある特定の関数を実装します。モジュールのようなものですが、クラスのコレクション専用です。ウィキペディアにはかなり良い説明があります: http://en.wikipedia.org/wiki/Visitor_pattern そして、彼らの例は私が言おうとしていることを説明するのに役立ちます.

それが少し解決するのに役立つことを願っています。

編集**申し訳ありませんが、私はあなたの答えのためにウィキペディアにリンクしましたが、彼らは本当にまともな例を持っています:)自分で見つけに行くと言う人になろうとはしていません.

于 2010-04-08T23:51:56.723 に答える
4

訪問者パターンの例。Book、Fruit、Vegetable は「Visitable」タイプの基本要素であり、BillingVisitor と OfferVisitorの 2 つの「Visitors」があります。それぞれの訪問者には独自の目的があります。請求書を計算するアルゴリズムと、これらの要素のオファーを計算するアルゴリズムがカプセル化されています。それぞれの訪問者と Visitables (要素) は同じままです。

import java.util.ArrayList;
import java.util.List;


public class VisitorPattern {

    public static void main(String[] args) {
        List<Visitable> visitableElements = new ArrayList<Visitable>();
        visitableElements.add(new Book("I123",10,2.0));
        visitableElements.add(new Fruit(5,7.0));
        visitableElements.add(new Vegetable(25,8.0));
        BillingVisitor billingVisitor = new BillingVisitor();
        for(Visitable visitableElement : visitableElements){
            visitableElement.accept(billingVisitor);
        }

        OfferVisitor offerVisitor = new OfferVisitor();
        for(Visitable visitableElement : visitableElements){
            visitableElement.accept(offerVisitor);
        }
        System.out.println("Total bill " + billingVisitor.totalPrice);
        System.out.println("Offer  " + offerVisitor.offer);

    }

    interface Visitor {
        void visit(Book book);
        void visit(Vegetable vegetable);
        void visit(Fruit fruit);
    }

    //Element
    interface Visitable{
        public void accept(Visitor visitor);
    }


    static class OfferVisitor implements Visitor{
        StringBuilder offer = new StringBuilder();

        @Override
        public void visit(Book book) {
            offer.append("Book " +  book.isbn +  " discount 10 %" + " \n");
        }

        @Override
        public void visit(Vegetable vegetable) {
            offer.append("Vegetable  No discount \n");
        }

        @Override
        public void visit(Fruit fruit) {
            offer.append("Fruits  No discount \n");
        }

    }

    static class BillingVisitor implements Visitor{
        double totalPrice = 0.0;

        @Override
        public void visit(Book book) {
            totalPrice += (book.quantity * book.price);
        }

        @Override
        public void visit(Vegetable vegetable) {
            totalPrice += (vegetable.weight * vegetable.price);
        }

        @Override
        public void visit(Fruit fruit) {
            totalPrice += (fruit.quantity * fruit.price);
        }

    }

    static class Book implements Visitable{
        private String isbn;
        private double quantity;
        private double price;

        public Book(String isbn, double quantity, double price) {
            this.isbn = isbn;
            this.quantity = quantity;
            this.price = price;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    }

    static class Fruit implements Visitable{
        private double quantity;
        private double price;

        public Fruit(double quantity, double price) {
            this.quantity = quantity;
            this.price = price;
        }

        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);
        }
    }

    static class Vegetable implements Visitable{
        private double weight;
        private double price;

        public Vegetable(double weight, double price) {
            this.weight = weight;
            this.price = price;
        }


        @Override
        public void accept(Visitor visitor) {
            visitor.visit(this);            
        }
    }


}
于 2014-11-23T13:10:18.977 に答える
1

データ操作を実際のデータから分離することです。ボーナスとして、クラスの階層全体に同じビジタークラスを再利用できます。これにより、実際のオブジェクトに関係のないデータ操作アルゴリズムを持ち歩く必要がなくなります。

于 2010-04-08T23:55:23.210 に答える