© 2025
Александр Легалов
Содержание
Посетитель (Visitor)
Посетитель описывает операцию, выполняемую с каждым объектом из некоторой структуры. Он позволяет определить новую операцию, не изменяя классы этих объектов. Используется обычно в следующих ситуациях:
- в структуре присутствуют объекты многих классов с различными интерфейсами, и нужно выполнять над ними операции, зависящие от конкретных классов;
- над объектами, входящими в состав структуры, должны выполняться разнообразные, не связанные между собой операции и вы не хотите «засорять» классы такими операциями. Посетитель позволяет объединить родственные операции, поместив их в один класс. Если структура объектов является общей для нескольких приложений, то паттерн посетитель позволит в каждое приложение включить только относящиеся к нему операции;
- классы, определяющие структуру объектов, изменяются редко, но новые операции над этой структурой добавляются часто. При изменении классов, представленных в структуре, придется переопределить интерфейсы всех посетителей, а это может вызвать затруднения. Поэтому если классы меняются достаточно часто, то, вероятно, лучше определить операции прямо в них.
Графическое представление ОО структуры Посетителя приведено на рисунке~\ref{visitor}.
Структура паттерна Посетитель
Объектно-ориентированная реализация Посетителя
Основная идея данного образца заключается в формировании независимых операций над некоторой группой зафиксированных альтернатив, порождаемых от общего базового класса. Общий родитель в данном случае нужен для того, чтобы можно было выходить на любого из потомков, используя ОО полиморфизм и вызывая через общую точку входа конкретный метод из посетителя с явным указанием типа потомка. Конкретный посетитель ориентирован на реализацию нескольких методов, каждый из которых обеспечивает обработку одной из альтернатив. Множество конкретных посетителей в результате реализуют наборы тематически независимых операций.
В нашем примере с животными посетители реализуют две независимые функции. Одна из них выводит информацию о том, что обычно ест каждое из животных, а вторая демонстрирует, как животное относится к плаванию. Для двух животных формируются четыре варианта поведения, образующие мультиметод, который реализуется через диспетчеризацию. Абстрактный посетитель определяет для каждой выполняемой функции интерфейс, обеспечивающий связи с соответствующими животными.
class Visitor {
public:
virtual void VisitDuck(const Duck *duck) const = 0;
virtual void VisitDog(const Dog *dog) const = 0;
};
Сами животные имеют метод Accept , предоставляющий доступ к одному из конкретных посетителей:
class Animal {
public:
virtual ~Animal() {}
virtual void Accept(Visitor *visitor) const = 0;
// ...
};
class Duck : public Animal {
public:
Duck() {std::cout << "Duck created!\n";}
void Accept(Visitor *visitor) const override {
visitor->VisitDuck(this);
}
std::string AboutDuck() const {
return "It is the Duck.";
}
};
class Dog : public Animal {
public:
Dog() {std::cout << "Dog created!\n";}
void Accept(Visitor *visitor) const override {
visitor->VisitDog(this);
}
std::string AboutDog() const {
return "It is the Dog.";
}
};
Конкретные посетители выполняют решение конкретных задач, завершая полиморфный обход выдачей необходимой информации.
// Посетитель, показывающий предпочтения в еде
class FeedVisitor : public Visitor {
public:
void VisitDuck(const Duck *duck) const override {
std::cout << duck->AboutDuck() << " It eats grain\n";
}
void VisitDog(const Dog *dog) const override {
std::cout << dog->AboutDog() << " It eats meat\n";
}
};
// Посетитель, демонстрирующий отношение к плаванию
class SwimVisitor : public Visitor {
public:
void VisitDuck(const Duck *duck) const override {
std::cout << duck->AboutDuck() << " It is very good swimmer.\n";
}
void VisitDog(const Dog *dog) const override {
std::cout << dog->AboutDog() << " It is not a good swimmer.\n";
}
};
Клиентский код обеспечивает вход через указатели на базовые классы, что в демонстрации, представленной главной функцией, позволяет перебрать возможные комбинации.
// Клиентский код выполняет операции над любыми комбинациями.
void Client(Animal* animal, Visitor *visitor) {
animal->Accept(visitor);
}
int main() {
Duck duck;
Dog dog;
FeedVisitor feeding;
SwimVisitor swimming;
Client(&duck, &feeding);
Client(&dog, &feeding);
Client(&duck, &swimming);
Client(&dog, &swimming);
return 0;
}
Процедурная схема Посетителя
Глядя на ОО схему Посетителя, думаешь: на что приходится идти, ради того, чтобы обойти отсутствие внешних функций? В процедурном подходе не нужно думать от том, как их добавить. А возникшие в ОО решении проблемы с добавлением альтернативных элементов по сути ничем не отличаются от тех проблем, которые имеются и в процедурном подходе. Правда, в отличие от процедурного подхода, при добавлении новых посетителей можно использовать полиморфизм. Но давайте посмотрим, что позволяет процедурно-параметрическое решение.
Графическое представление процедурной структуры Посетителя приведено на рисунке. Можно видеть, что в данной ситуации имеется пара независимых альтернатив, образующих соответствующие комбинации. Мультиметод так и напрашивается. Но ничто не мешает использовать диспетчеризацию, как и в ООП. Оставим мультиметод на десерт и начнем с решения, аналогичного объектно-ориентированной реализации.
Процедурная схема паттерна Посетитель
Прямая процедурно-параметрическая имитация Посетителя
Не представляет трудностей непосредственное повторение ОО схемы. Утки и собаки, как специализации животных реализуются по уже традиционной схеме. Функции, обеспечивающие инициализацию, повторяют действия, определяемые в предыдущей программе конструкторами классов.
// Утка
typedef struct Duck {} Duck;
// Информация об утке
char* AboutDuck(Duck* a) {
return "It is the Duck.";
}
// Собака
typedef struct Dog {} Dog;
// Информация об утке
char* AboutDog(Dog* a) {
return "It is the Dog.";
}
// Обобщение для животных.
typedef struct Animal {}<> Animal;
// Специализации животных
Animal + <Duck;>;
Animal + <Dog;>;
// Обобщенная инициализация животных (пока пустая)
void Init<Animal* a>() = 0;
// Специализированные инициализаторы
void Init<Animal.Duck* a>() {
printf("Duck created!\n");
}
void Init<Animal.Dog* a>() {
printf("Dog created!\n");
}
Посетители, как и в других случаях, вместо интерфейсов задаются через эволюционно расширяемый перечислимый тип, а методы класса превращаются во внешние обобщающие функции и связанные с ними обработчики специализаций.
// Обобщенный посетитель
typedef struct Visitor {}<> Visitor;
// Специализации посетителя
Visitor + <Feed: void;>;
Visitor + <Swim: void;>;
// Обобщающие функции Посетителя
// Посещение утки
void VisitDuck<Visitor* v>(Duck* d) = 0;
// Обработчики специализаций посетителя
void VisitDuck<Visitor.Feed* v>(Duck* d) {
printf("%s It eats grain\n", AboutDuck(d));
}
void VisitDuck<Visitor.Swim* v>(Duck* d) {
printf("%s It is very good swimmer.\n", AboutDuck(d));
}
// Посещение собаки
void VisitDog<Visitor* v>(Dog* d) = 0;
// Обработчики специализаций посетителя
void VisitDog<Visitor.Feed* v>(Dog* d) {
printf("%s It eats meat\n", AboutDog(d));
}
void VisitDog<Visitor.Swim* v>(Dog* d) {
printf("%s It is not a good swimmer.\n", AboutDog(d));
}
Также отдельными обобщающими функциями представлены функции доступа к посетителям, которые и запускают диспетчеризацию:
// Обобщающая функция для запуска диспетчеризации
void Accept<Animal* animal>(Visitor *visitor) = 0;
// Обработчики специализаций
void Accept<Animal.Duck* animal>(Visitor *visitor) {
VisitDuck<visitor>(&(animal->@));
}
void Accept<Animal.Dog* animal>(Visitor *visitor) {
VisitDog<visitor>(&(animal->@));
}
Клиентский код и главная функция, поменявшись синтаксически, также аналогичны по действиям ОО решению:
// Клиентский код может выполнять операции посетителя над любым набором.
void Client(Animal* animal, Visitor *visitor) {
Accept<animal>(visitor);
}
int main() {
struct Animal.Duck duck; Init<(Animal*)&duck>();
struct Animal.Dog dog; Init<(Animal*)&dog>();
struct Visitor.Feed feeding;
struct Visitor.Swim swimming;
Client((Animal*)&duck, (Visitor*)&feeding);
Client((Animal*)&dog, (Visitor*)&feeding);
Client((Animal*)&duck, (Visitor*)&swimming);
Client((Animal*)&dog, (Visitor*)&swimming);
return 0;
}
Реверсивная процедурно-параметрическая имитация Посетителя
По своей сути представленное ПП решение соответствует использованию в процедурном подходе вложенных переключателей, где внешний "свич" анализирует тип животного, а внутренний выбирает выполняемую над ним функцию. Но симметрия задачи не мешает поменять переключатели местами. По сути от перемены мест переключателей (не слагаемых) функциональность не меняется. Но почему бы иногда не начинать работу Посетителя с выбора функции, а уже затем животного? ПП подход также легко позволяет переориентироваться на альтернативное решение, поменяв местами доступ в рассмотренной выше диспетчеризации. Меняются только функции. Данные остаются неприкосновенными. При этом все изменения для потребителей посетителя будут скрыты внутри клиентов. Изменения могут быть представлены следующим кодом:
// Обобщающие функции Посетителя
// Посещение еды
void VisitFeed<Animal* a>() = 0;
// Обработчики специализаций посетителя
void VisitFeed<Animal.Duck* a>() {
printf("%s It eats grain\n", AboutDuck(&(a->@)));
}
void VisitFeed<Animal.Dog* a>() {
printf("%s It eats meat\n", AboutDog(&(a->@)));
}
// Посещение плавания
void VisitSwim<Animal* a>() = 0;
// Обработчики специализаций посетителя
void VisitSwim<Animal.Duck* a>() {
printf("%s It is very good swimmer.\n", AboutDuck(&(a->@)));
}
void VisitSwim<Animal.Dog* a>() {
printf("%s It is not a good swimmer.\n", AboutDog(&(a->@)));
}
// Обобщающая функция для запуска реверсивной диспетчеризации
void ReverseAccept<Visitor *visitor>(Animal* animal) = 0;
// Обработчики специализаций
void ReverseAccept<Visitor.Feed *visitor>(Animal* animal) {
VisitFeed<animal>();
}
void ReverseAccept<Visitor.Swim *visitor>(Animal* animal) {
VisitSwim<animal>();
}
// Клиентский код может выполнять операции посетителя над любым набором.
void Client(Animal* animal, Visitor *visitor) {
ReverseAccept<visitor>(animal);
}
Интересно: а во что выльется подобная трактовка при ОО подходе? Будет ли это альтернативный паттерн или черт знает что?
Имитация Посетителя как мультиметода
И немного о прямой реализации мультиметода. В общем то и писать особо не о чем. Просто берешь и реализуешь в лоб схему, представленную выше на рисунке. Если опять сопоставить с процедурным подходом, то можно в качестве аналогии привести одноуровневые условные оператооры, одновременно проверяющие по конъюнкции пару условий:
if((животное == X) && (функция == Y))...
В результате Посетитель вырождается в прозрачный код выбора действия над каждым из животных, но без каких--либо промежуточных телодвижений типа Access :
// Обобщающие функции, реализованные с использованием мультиметода
// Посещение животного и действие над ним
void Visit<Animal* a, Visitor* v>() = 0;
// Обработчики специализаций посетителя
void Visit<Animal.Duck* a, Visitor.Feed* v>() {
printf("%s It eats grain\n", AboutDuck(&(a->@)));
}
void Visit<Animal.Duck* a, Visitor.Swim* v>() {
printf("%s It is very good swimmer.\n", AboutDuck(&(a->@)));
}
void Visit<Animal.Dog* a, Visitor.Feed* v>() {
printf("%s It eats meat\n", AboutDog(&(a->@)));
}
void Visit<Animal.Dog* a, Visitor.Swim* v>() {
printf("%s It is not a good swimmer.\n", AboutDog(&(a->@)));
}
// Клиентский код может выполнять операции посетителя над любым набором.
void Client(Animal* animal, Visitor *visitor) {
Visit<animal, visitor>();
}
Все остальное также, как при прямой и реверсивной диспетчеризациях. Так и хочется воскликнуть после этого: Как тебе такое, Банда Четырех?
Содержание
|