Давным-давно ...
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; }
}
Вы отделяете данные от алгоритма. Вы переносите алгоритм на реализацию посетителя. Вы добавляете функциональность, создавая дополнительных посетителей, вместо того, чтобы постоянно изменять (и раздувать) класс, в котором хранятся данные.
Он обеспечивает еще один уровень абстракции. Снижает сложность объекта и делает его более модульным. Сорта похожа на использование интерфейса (реализация полностью независима, и никто не заботится о том, как это делается, просто о том, что это делается.)
Я никогда не использовал ее, но она была бы полезна для: Реализации определенной функции, которая должна быть выполнена. в разных подклассах, поскольку каждый из подклассов должен реализовывать его по-разному, другой класс будет реализовывать все функции. Вроде как модуль, но только для набора классов. В Википедии есть довольно хорошее объяснение: http://en.wikipedia.org/wiki/Visitor_pattern И их пример помогает объяснить то, что я пытаюсь сказать.
Надеюсь, что это поможет немного прояснить ситуацию.
РЕДАКТИРОВАТЬ ** Извините, я связался с википедией для вашего ответа, но у них действительно есть достойный пример :) Не пытаюсь быть тем парнем, который говорит, иди и найди сам.
Он предназначен для отделения обработки данных от фактических данных. В качестве бонуса вы можете повторно использовать один и тот же класс посетителей для всей иерархии ваших классов, что снова избавляет вас от необходимости носить с собой алгоритмы манипулирования данными, которые не имеют отношения к вашим реальным объектам.
Итак, вы, вероятно, прочитали миллиард различных объяснений паттерна visitor, и вы, вероятно, все еще говорите: "Но когда бы вы его использовали!"
Традиционно visitors используются для реализации тестирования типов без ущерба для безопасности типов, при условии, что ваши типы хорошо определены и известны заранее. Допустим, у нас есть несколько классов:
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);
}
Он работает, но есть много проблем с этим кодом:
Паттерн Visitor решает эту проблему элегантно. Начнем с модификации нашего базового класса 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);
Это имеет следующие преимущества:
С учетом сказанного, посетители обычно излишни, они имеют тенденцию сильно усложнять 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.