Цель шаблона посетителя с примерами
Я действительно запутался по поводу шаблона посетителя и его использования. Я не могу представить себе преимущества использования этого шаблона или его цели. Если бы кто-то мог объяснить с примерами, если это возможно, это было бы здорово.
6 ответов
Давным-давно...
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; }
}
Вы отделяете данные от алгоритма. Вы разгрузите алгоритм для реализации посетителя. Вы добавляете функциональность, создавая больше посетителей, вместо того, чтобы постоянно изменять (и раздувать) класс, содержащий данные.
Таким образом, вы, вероятно, прочитали различные объяснения модели посетителей, и, вероятно, все еще говорите: "Но когда бы вы ее использовали!"
Традиционно посетители используются для проведения тестирования типов без ущерба для безопасности типов, при условии, что ваши типы четко определены заранее и известны заранее. Допустим, у нас есть несколько классов следующим образом:
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, и вы хотите дать потребителям способ добавить новые специальные функции для объектов. Они также являются удобным способом расширения закрытых классов с помощью специальных функций.
Вы выполняете операции с классом объектов и хотите избежать тестирования типов во время выполнения. Это обычно тот случай, когда вы пересекаете иерархию разнородных объектов, имеющих разные свойства.
Не используйте посетителей, когда:
Вы поддерживаете операции с классом объектов, чьи производные типы не известны заранее.
Операции над объектами заранее четко определены, особенно если они могут быть унаследованы от базового класса или определены в интерфейсе.
Клиентам проще добавлять новые функции в классы, используя наследование.
Вы пересекаете иерархию объектов, которые имеют одинаковые свойства или интерфейс.
Вы хотите относительно простой API.
Это обеспечивает еще один уровень абстракции. Уменьшает сложность объекта и делает его более модульным. Сорту нравится использовать интерфейс (реализация полностью независима, и никому нет дела до того, как это делается, только когда это делается).
Сейчас я никогда не использовал его, но это было бы полезно для: Реализации конкретной функции, которая должна выполняться в разных подклассах, поскольку каждый из подклассов должен реализовывать ее по-разному, а другой класс будет реализовывать все функции. Вроде как модуль, но только для коллекции классов. В Википедии есть довольно хорошее объяснение: http://en.wikipedia.org/wiki/Visitor_pattern И их пример помогает объяснить то, что я пытаюсь сказать.
Надеюсь, это поможет немного прояснить ситуацию.
EDIT**Sorry I linked to wikipedia for your answer but they really do have a decent example:) Not trying to be that guy that says go find it yourself.
Пример шаблона посетителя. Book, Fruit & Vegetable являются основными элементами типа "Visitable", и есть два "Посетителя", 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);
}
}
}
Я думаю, что основная цель шаблона посетителя - это его высокая расширяемость. Интуиция в том, что ты купил робота. Робот уже полностью реализовал элементарные функции: идти вперед, поворачивать налево, поворачивать направо, идти назад, что-то выбирать, говорить на этапе…
Однажды, вы хотите, чтобы ваш робот мог пойти на почту для вас. Со всеми этими элементарными функциями это можно сделать, но вам нужно доставить своего робота в магазин и "обновить" своего робота. Продавцу магазина не нужно модифицировать робота, просто установите новый чип обновления для своего робота, и он сможет делать то, что вы хотите.
В другой день вы хотите, чтобы ваш робот пошел в супермаркет. Тот же процесс, вы должны принести своего робота в магазин и обновить эту "расширенную" функциональность. Не нужно модифицировать самого робота.
и так далее …
Таким образом, идея шаблона посетителя заключается в том, что, учитывая все реализованные элементарные функции, вы можете использовать шаблон посетителя для добавления бесконечного числа сложных функций. В этом примере робот - это ваши рабочие классы, а "чип обновления" - это посетители. Каждый раз, когда вам нужно новое "обновление" функциональности, вы не изменяете свой рабочий класс, а добавляете посетителя.
Это для отделения манипулирования данными от фактических данных. В качестве бонуса вы можете повторно использовать один и тот же класс посетителей для всей иерархии ваших классов, что снова избавляет вас от необходимости использовать алгоритмы манипулирования данными, которые не имеют отношения к вашим реальным объектам.