Цель шаблона посетителя с примерами

Я действительно смущен о шаблоне посетителя и его использовании. На самом деле я не могу представить себе преимущества использования этого шаблона или его цели. Если кто-то может объяснить с примерами, если это возможно, это было бы здорово.

Ответ 1

Когда-то...

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

Вы отделяете данные от алгоритма. Вы разгружаете алгоритм реализации посетителя. Вы добавляете функциональность, создавая больше посетителей, вместо того, чтобы постоянно изменять (и раздувать) класс, содержащий данные.

Ответ 2

Таким образом, вы, вероятно, читали бай-дион различных объяснений шаблона посетителя, и вы, вероятно, все еще говорите "но когда вы его используете!"

Традиционно посетители используют для тестирования типов без ущерба для безопасности типа, если ваши типы четко определены заранее и известны заранее. Скажем, у нас есть несколько классов следующим образом:

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

Я хочу разбить список в трех списках, каждый из которых содержит апельсины, яблоки или бананы. Как бы вы это сделали? Ну, простое решение было бы типом-тестом:

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, и вы хотите дать потребителям способ добавления новых специальных функций к объектам. Они также являются удобным способом расширения закрытых классов с помощью ad-hoc functionaity.

  • Вы выполняете операции класса объектов и хотите избежать тестирования типа времени выполнения. Обычно это происходит, когда вы проходите иерархию разрозненных объектов, имеющих разные свойства.

Не использовать посетителей, когда:

  • Вы поддерживаете операции над классом объектов, производные типы которых неизвестны заранее.

  • Операции над объектами четко определены заранее, особенно если они могут быть унаследованы от базового класса или определены в интерфейсе.

  • Легче для клиентов добавлять новые функции в классы с использованием наследования.

  • Вы просматриваете иерархию объектов, которые имеют одинаковые свойства или интерфейс.

  • Вам нужен относительно простой API.

Ответ 3

Он предоставляет другой уровень абстракции. Уменьшает сложность объекта и делает его более модульным. Сорта вроде использования интерфейса (реализация полностью независима, и никто не заботится о том, как это делается только для того, чтобы это было сделано.)

Теперь я никогда не использовал его, но это было бы полезно для: Реализация конкретной функции, которая должна выполняться в разных подклассах, поскольку каждый из подкласс должен реализовывать ее разными способами, другой класс будет реализовывать все функции. Kinda как модуль, но только для набора классов. Википедия имеет довольно хорошее объяснение: http://en.wikipedia.org/wiki/Visitor_pattern И их пример помогает объяснить, что я пытаюсь сказать.

Надеюсь, что это поможет немного разобраться.

EDIT ** Извините, что я связался с wikipedia для вашего ответа, но у них действительно есть достойный пример:) Не пытайтесь быть тем парнем, который говорит, что идите сами.

Ответ 4

Пример шаблона посетителя. Книга, фрукты и овощи являются основными элементами типа "Visitable" и есть два "Посетителя" , BillingVisitor и OfferVisitor. У каждого посетителя есть своя цель. Чтобы рассчитать счет и алго для расчета предложений по этим элементам, инкапсулирован в соответствующем посетителе и посетителях (Элементы) остаются теми же.

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


}

Ответ 5

Отделить данные от фактических данных. В качестве бонуса вы можете повторно использовать один и тот же класс посетителей для всей иерархии ваших классов, что еще раз избавит вас от переноса алгоритмов манипуляции данными, которые не имеют отношения к вашим реальным объектам.

Ответ 6

Я думаю, что основная цель шаблона посетителей - это высокая расширяемость. В интуиции вы купили робота. Робот уже полностью реализовал элементарные функции, как идти вперед, повернуть налево, повернуть направо, вернуться, выбрать что-то, говорить на фазе,...

В один прекрасный день вы хотите, чтобы ваш робот мог отправиться в почтовое отделение для вас. Со всеми этими элементарными функциями он может это сделать, но вам нужно принести вам робота в магазин и "обновить" вашего робота. Продавцу магазина не нужно модифицировать робота, но просто поместите новый чип обновления вашему роботу и он может делать то, что вы хотите.

В другой день вы хотите, чтобы ваш робот отправился в супермаркет. Тот же процесс, вы должны привести своего робота в магазин и обновить эту "расширенную" функциональность. Не нужно изменять сам робот.

и т.д.

Таким образом, идея шаблона Visitor, учитывая все реализованные элементарные функции, вы можете использовать шаблон посетителя для добавления бесконечного количества сложных функций. В этом примере робот - это ваши рабочие классы, а "чип обновления" - посетители. Каждый раз, когда требуется новое "обновление" функциональности, вы не изменяете свой рабочий класс, но добавляете посетителя.