Шаблон Посетитель служит для выполнения операций над всеми объектами, объединёнными в некоторую структуру. При этом выполняемые операции не являться частью объектов структуры.
Часто в программах встречаются сложные структуры, представляющие собой дерево или граф и состоящие из разнотипных узлов. При этом имеется необходимость обрабатывать все узлы графа или дерева. Очевидное решение – добавить в базовый класс узла виртуальный метод, перекрываемый в наследниках для выполнения нужного действия. Код, который приведён ниже, демонстрирует описанный подход.
// базовый класс для узла
public abstract class BaseNode
{
public string Name { get; private set; }
protected BaseNode(string name)
{
Name = name;
}
public abstract void Print(TextWriter writer);
}
// простой узел
public class SimpleNode: BaseNode
{
public SimpleNode(string name): base(name)
{
}
public override void Print(TextWriter writer)
{
writer.WriteLine(Name + ": Simple");
}
}
// узел с дочерними узлами
public class CompositeNode: BaseNode
{
public IList<BaseNode> Nodes { get; private set; }
public CompositeNode(string name, params BaseNode[] nodes):
base(name)
{
Nodes = Array.AsReadOnly(nodes);
}
public override void Print(TextWriter writer)
|
|
{
writer.WriteLine(Name + ": Composite");
foreach (var node in Nodes)
{
node.Print(writer);
}
}
}
// использование классов - построение и печать иерархии
var tree = new CompositeNode("Root",
new CompositeNode("Level_1",
new SimpleNode("Leaf_1_1"),
new SimpleNode("Leaf_1_2")),
new SimpleNode("Leaf_2"));
tree.Print(Console.Out);
Решение, применяемое в примере, имеет серьёзный недостаток: в нем структура данных оказывается увязанной с обрабатывающими её алгоритмами. Если нам понадобится алгоритм, отличный от реализованного, то придётся добавлять ещё один виртуальный метод. Ещё хуже, если классы, описывающие дерево, содержатся в недоступном для модификации коде.
Основная идея шаблона Посетитель состоит в том, что каждый элемент объектной структуры содержит виртуальный метод Accept(), который принимает на вход в качестве аргумента специальный объект – посетитель, реализующий заранее известный интерфейс. Этот интерфейс содержит по одному методу Visit() для каждого типа узла. Метод Accept() в каждом узле должен вызывать методы Visit() для осуществления навигации по структуре.
// интерфейс посетителя
public interface IVisitor
{
void Visit(SimpleNode node);
void Visit(CompositeNode node);
}
// конкретный посетитель
public class PrintVisitor: IVisitor
{
private readonly TextWriter _writer;
public PrintVisitor(TextWriter writer)
{
_writer = writer;
}
public void Visit(SimpleNode node)
{
_writer.WriteLine(node.Name + ": Simple");
}
public void Visit(CompositeNode node)
{
_writer.WriteLine(node.Name + ": Composite");
foreach (var node in node.Nodes)
node.Accept(this);
}
}
public abstract class BaseNode
{
public string Name { get; private set; }
protected BaseNode(string name)
{
Name = name;
}
public abstract void Accept(IVisitor visitor);
}
public class SimpleNode: BaseNode
{
public SimpleNode(string name): base(name)
{
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
|
|
}
}
public class CompositeNode: BaseNode
{
public IList<BaseNode> Nodes { get; private set; }
public CompositeNode(string name, params BaseNode[] nodes):
base(name)
{
Nodes = Array.AsReadOnly(nodes);
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
Приём, использованный в примере, носит название двойная диспетчеризация. Обычные виртуальные методы используют одинарную диспетчеризацию – то, какая операция будет выполнена, зависит от имени метода и типа объекта-получателя. В двойной диспетчеризации шаблона Посетитель вызываемая операция зависит от имени метода, типа посещаемого элемента и типа конкретного посетителя[5].