Паттерн Visitor (посетитель)
Назначение паттерна Visitor
- Паттерн Visitor определяет операцию, выполняемую на каждом элементе из некоторой структуры. Позволяет, не изменяя классы этих объектов, добавлять в них новые операции.
- Является классической техникой для восстановления потерянной информации о типе.
- Паттерн Visitor позволяет выполнить нужные действия в зависимости от типов двух объектов.
- Предоставляет механизм двойной диспетчеризации.
Решаемая проблема
Различные и несвязанные операции должны выполняться над узловыми объектами некоторой гетерогенной совокупной структуры. Вы хотите избежать "загрязнения" классов этих узлов такими операциями (то есть избежать добавления соответствующих методов в эти классы). И вы не хотите запрашивать тип каждого узла и осуществлять приведение указателя к правильному типу, прежде чем выполнить нужную операцию.
Обсуждение паттерна Visitor
Основным назначением паттерна Visitor является введение абстрактной функциональности для совокупной иерархической структуры объектов "элемент", а именно, паттерн Visitor позволяет, не изменяя классы Element
, добавлять в них новые операции. Для этого вся обрабатывающая функциональность переносится из самих классов Element
(эти классы становятся "легковесными") в иерархию наследования Visitor
.
При этом паттерн Visitor использует технику "двойной диспетчеризации". Обычно при передаче запросов используется "одинарная диспетчеризация" – то, какая операция будет выполнена для обработки запроса, зависит от имени запроса и типа получателя. В "двойной диспетчеризации" вызываемая операция зависит от имени запроса и типов двух получателей (типа Visitor
и типа посещаемого элемента Element
).
Реализуйте паттерн Visitor следующим образом. Создайте иерархию классов Visitor
, в абстрактном базовом классе которой для каждого подкласса Element
совокупной структуры определяется чисто виртуальный метод visit()
. Каждый метод visit()
принимает один аргумент - указатель или ссылку на подкласс Element
.
Каждая новая добавляемая операция моделируется при помощи конкретного подкласса Visitor
. Подклассы Visitor
реализуют visit()
методы, объявленные в базовом классе Visitor
.
Добавьте один чисто виртуальный метод accept()
в базовый класс иерархии Element
. В качестве параметра accept()
принимает единственный аргумент - указатель или ссылку на абстрактный базовый класс иерархии Visitor
.
Каждый конкретный подкласс Element
реализует метод accept()
следующим образом: используя полученный в качестве параметра адрес экземпляра подкласса Visitor
, просто вызывает его метод visit()
, передавая в качестве единственного параметра указатель this
.
Теперь "элементы" и "посетители" готовы. Если клиенту нужно выполнить какую-либо операцию, то он создает экземпляр объекта соответствующего подкласса Visitor
и вызывает accept()
метод для каждого объекта Element
, передавая экземпляр Visitor
в качестве параметра.
При вызове метода accept()
ищется правильный подкласс Element
. Затем, при вызове метода visit()
программное управление передается правильному подклассу Visitor
. Таким образом, двойная диспетчеризация получается как сумма одинарных диспетчеризаций сначала в методе accept()
, а затем в методе visit()
.
Паттерн Visitor позволяет легко добавлять новые операции – нужно просто добавить новый производный от Visitor
класс. Однако паттерн Visitor следует использовать только в том случае, если подклассы Element
совокупной иерархической структуры остаются стабильными (неизменяемыми). В противном случае, нужно приложить значительные усилия на обновление всей иерархии Visitor
.
Иногда приводятся возражения по поводу использования паттерна Visitor, поскольку он разделяет данные и алгоритмы, что противоречит концепции объектно-ориентированного программирования. Однако успешный опыт применения STL, где разделение данных и алгоритмов положено в основу, доказывает возможность использования паттерна Visitor.
Структура паттерна Visitor
Несмотря на то, что реализации метода accept()
в подклассах иерархии Element
всегда одинаковая, этот метод не может быть перенесен в базовый класс Element
и наследоваться производными классами. В этом случае адрес, получаемый с помощью указателя this
, будет всегда соответствовать базовому типу Element
.
UML-диаграмма классов паттерна Visitor
При вызове полиморфного метода firstDispatch()
у объекта абстрактного типа First
"восстанавливается" конкретный тип этого объекта. Когда вызывается полиморфный метод secondDispatch()
объекта абстрактного типа Second
, "восстанавливается" его конкретный тип. Теперь может выполняться функциональность, соответствующая этой паре типов.
Пример паттерна Visitor
Паттерн Visitor определяет операцию, выполняемую на каждом элементе из некоторой структуры без изменения классов этих объектов. Таксомоторная компания использует этот паттерн в своей работе. Когда клиент звонит в такую компанию, диспетчер отправляет к нему свободное такси. После того как клиент садится в такси, его доставляют до места.
Использование паттерна Visitor
- Убедитесь, что текущая иерархия Element будет оставаться стабильной и, что открытый интерфейс этих классов достаточно эффективен для доступа классов
Visitor
. Если это не так, то паттерн Visitor – не очень хорошее решение. - Создайте базовый класс
Visitor
c методамиvisit(ElementXxx)
для каждого подкласса Element. Добавьте метод
accept(Visitor)
в иерархиюElement
. Реализация этого метода во всех подклассахElement
всегда одна и та же –accept( Visitor v ) { v.visit( this ); }
. Из-за циклических зависимостей объявления классовElement
иVisitor
должны чередоваться.Иерархия Element связана только с базовым классом
Visitor
, в то время как иерархияVisitor
связана с каждым производным отElement
классом. Если стабильность иерархииElement
низкая, а стабильность иерархииVisitor
высокая, рассмотрите возможность обмена "ролей" этих двух иерархий.Для каждой "операции", которая должна выполняться для объектов
Element
, создайте производный отVisitor
класс. Реализации методаvisit()
должны использовать открытый интерфейс классаElement
.Клиенты создают объекты
Visitor
и передают их каждому объектуElement
, вызываяaccept()
.
Особенности паттерна Visitor
- Совокупная структура объектов
Elements
может определяться с помощью паттерна Composite. - Для обхода Composite может использоваться Iterator.
- Паттерн Visitor демонстрирует классический прием восстановления информации о потерянных типах, не прибегая к понижающему приведению типов (dynamic cast).
Реализация паттерна Visitor
Реализация паттерна Visitor по шагам
- Добавьте метод
accept(Visitor)
иерархию "элемент". - Создайте базовый класс
Visitor
и определите методыvisit()
для каждого типа "элемента". - Создайте производные классы
Visitor
для каждой "операции", исполняемой над "элементами". - Клиент создает объект
Visitor
и передает его в вызываемый методaccept()
.
#include <iostream>
#include <string>
using namespace std;
// 1. Добавьте метод accept(Visitor) иерархию "элемент"
class Element
{
public:
virtual void accept(class Visitor &v) = 0;
};
class This: public Element
{
public:
/*virtual*/void accept(Visitor &v);
string thiss()
{
return "This";
}
};
class That: public Element
{
public:
/*virtual*/void accept(Visitor &v);
string that()
{
return "That";
}
};
class TheOther: public Element
{
public:
/*virtual*/void accept(Visitor &v);
string theOther()
{
return "TheOther";
}
};
// 2. Создайте базовый класс Visitor и определите
// методы visit()для каждого типа "элемента"
class Visitor
{
public:
virtual void visit(This *e) = 0;
virtual void visit(That *e) = 0;
virtual void visit(TheOther *e) = 0;
};
/*virtual*/void This::accept(Visitor &v)
{
v.visit(this);
}
/*virtual*/void That::accept(Visitor &v)
{
v.visit(this);
}
/*virtual*/void TheOther::accept(Visitor &v)
{
v.visit(this);
}
// 3. Создайте производные классы Visitor для каждой
// "операции", исполняемой над "элементами"
class UpVisitor: public Visitor
{
/*virtual*/void visit(This *e)
{
cout << "do Up on " + e->thiss() << '\n';
}
/*virtual*/void visit(That *e)
{
cout << "do Up on " + e->that() << '\n';
}
/*virtual*/void visit(TheOther *e)
{
cout << "do Up on " + e->theOther() << '\n';
}
};
class DownVisitor: public Visitor
{
/*virtual*/void visit(This *e)
{
cout << "do Down on " + e->thiss() << '\n';
}
/*virtual*/void visit(That *e)
{
cout << "do Down on " + e->that() << '\n';
}
/*virtual*/void visit(TheOther *e)
{
cout << "do Down on " + e->theOther() << '\n';
}
};
int main()
{
Element *list[] =
{
new This(), new That(), new TheOther()
};
UpVisitor up; // 4. Клиент создает
DownVisitor down; // объекты Visitor
for (int i = 0; i < 3; i++)
// и передает каждый
list[i]->accept(up);
for (i = 0; i < 3; i++)
// в вызываемый метод accept()
list[i]->accept(down);
}
Вывод программы:
do Up on This do Down on This do Up on That
do Down on That do Up on TheOther do Down on TheOther
Реализация паттерна Visitor: до и после
До
Интерфейс для "операций" определяется в базовом классе Color и реализуется в его подклассах.
class Color
{
public:
virtual void count() = 0;
virtual void call() = 0;
static void report_num()
{
cout << "Reds " << s_num_red << ", Blus " << s_num_blu << '\n';
}
protected:
static int s_num_red, s_num_blu;
};
int Color::s_num_red = 0;
int Color::s_num_blu = 0;
class Red: public Color
{
public:
void count()
{
++s_num_red;
}
void call()
{
eye();
}
void eye()
{
cout << "Red::eye\n";
}
};
class Blu: public Color
{
public:
void count()
{
++s_num_blu;
}
void call()
{
sky();
}
void sky()
{
cout << "Blu::sky\n";
}
};
int main()
{
Color *set[] =
{
new Red, new Blu, new Blu, new Red, new Red, 0
};
for (int i = 0; set[i]; ++i)
{
set[i]->count();
set[i]->call();
}
Color::report_num();
}
Вывод программы:
Red::eye Blu::sky Blu::sky Red::eye Red::eye Reds 3, Blus 2
После
Иерархия Color
определяет единственный метод accept()
, а методы count()
и call()
реализованы в виде производных классов Visitor
. При вызове метода accept()
объекта Color
происходит первая диспетчеризация, а при вызове метода visit()
объекта Visitor
– вторая. После этого на основе типов обоих объектов могут выполняться все необходимые действия.
class Color
{
public:
virtual void accept(class Visitor*) = 0;
};
class Red: public Color
{
public:
/*virtual*/void accept(Visitor*);
void eye()
{
cout << "Red::eye\n";
}
};
class Blu: public Color
{
public:
/*virtual*/void accept(Visitor*);
void sky()
{
cout << "Blu::sky\n";
}
};
class Visitor
{
public:
virtual void visit(Red*) = 0;
virtual void visit(Blu*) = 0;
};
class CountVisitor: public Visitor
{
public:
CountVisitor()
{
m_num_red = m_num_blu = 0;
}
/*virtual*/void visit(Red*)
{
++m_num_red;
}
/*virtual*/void visit(Blu*)
{
++m_num_blu;
}
void report_num()
{
cout << "Reds " << m_num_red << ", Blus " << m_num_blu << '\n';
}
private:
int m_num_red, m_num_blu;
};
class CallVisitor: public Visitor
{
public:
/*virtual*/void visit(Red *r)
{
r->eye();
}
/*virtual*/void visit(Blu *b)
{
b->sky();
}
};
void Red::accept(Visitor *v)
{
v->visit(this);
}
void Blu::accept(Visitor *v)
{
v->visit(this);
}
int main()
{
Color *set[] =
{
new Red, new Blu, new Blu, new Red, new Red, 0
};
CountVisitor count_operation;
CallVisitor call_operation;
for (int i = 0; set[i]; i++)
{
set[i]->accept(&count_operation);
set[i]->accept(&call_operation);
}
count_operation.report_num();
}
Вывод программы:
Red::eye Blu::sky Blu::sky Red::eye Red::eye Reds 3, Blus 2