web-dev-qa-db-ja.com

訪問者パターンの目的と例

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

80
Victor

昔々...

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

次に、ライブラリのコレクションを他のジャンルでフィルター処理できるようにしたいことがわかります。新しいゲッターメソッドを追加し続けることができます。または、訪問者を使用できます。

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

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

64
Noel Ang

そのため、おそらく訪問者パターンのさまざまな説明を読んだことがありますが、おそらく「しかし、いつそれを使用するのでしょうか」と言っているでしょう。

従来、訪問者は、タイプが事前に明確に定義され、事前に知られている限り、タイプセーフティを犠牲にすることなくタイプテストを実装するために使用されます。次のようないくつかのクラスがあるとしましょう:

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つのリストに分割し、それぞれにオレンジ、リンゴ、またはバナナが含まれているようにします。どうしますか?さて、easyソリューションは型テストになります:

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

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

  • まず、Forいです。
  • タイプセーフではないため、実行時までタイプエラーをキャッチしません。
  • メンテナンスできません。 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); } }

貼り付けコードをコピーしているように見えますが、派生クラスはすべて異なるオーバーロードを呼び出していることに注意してください(AppleVisit(Apple)を呼び出し、BananaVisit(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が必要です。

186
Juliet

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

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

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

編集**あなたの答えのためにウィキペディアにリンクしましたが、実際にはまともな例があります:).

6
Kaili

訪問者パターンの例。 Book、Fruit&Vegetableは、タイプ"Visitable"の基本要素であり、2つの"Visitors"BillingVisitor&OfferVisitorがあります。請求書を計算するアルゴリズムと、これらの要素のオファーを計算するアルゴリズムは、それぞれの訪問者にカプセル化され、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);            
        }
    }


}
4
Shakti

訪問者パターンの主な目的は、拡張性が高いことだと思います。直感はあなたがロボットを買ったということです。ロボットは、先に進む、左に曲がる、右に曲がる、戻る、何かを選ぶ、フェーズを話す、といった基本的な機能を既に完全に実装しています。

ある日、あなたのロボットがあなたのために郵便局に行くことができることを望みます。これらの基本的な機能はすべて実行できますが、ロボットをショップに持ち込み、ロボットを「更新」する必要があります。ショップの売り手はロボットを変更する必要はありませんが、新しいアップデートチップをロボットに装着するだけで、望みどおりのことができます。

先日、ロボットをスーパーマーケットに行きたいと思っています。同じプロセスで、ロボットをショップに持ち込み、この「高度な」機能を更新する必要があります。ロボット自体を変更する必要はありません。

等々 …

そのため、Visitorパターンの考え方は、実装されているすべての基本機能を前提として、visitorパターンを使用して無限の洗練された機能を追加できます。この例では、ロボットはワーカークラスであり、「更新チップ」は訪問者です。機能の新しい「更新」が必要になるたびに、ワーカークラスを変更するのではなく、ビジターを追加します。

3
ctNGUYEN

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

1
Egor Pavlikhin