Ответ
Посетитель — это поведенческий паттерн, который позволяет добавлять новые операции к целым иерархиям классов, не изменяя сами эти классы. Он реализует принцип двойной диспетчеризации: операция выполняется в зависимости от типов как самого посетителя, так и посещаемого элемента.
Ключевая идея: Вы выносите набор родственных операций (например, рендеринг, экспорт, валидация) в отдельные классы-посетители. Каждый класс иерархии должен всего лишь один раз реализовать метод accept(Visitor&), который делегирует выполнение посетителю.
Пример: операции над геометрическими фигурами
#include <iostream>
#include <vector>
// Предварительные объявления и интерфейс Посетителя
class Circle;
class Rectangle;
class ShapeVisitor {
public:
virtual void visit(Circle& circle) = 0;
virtual void visit(Rectangle& rect) = 0;
virtual ~ShapeVisitor() = default;
};
// Интерфейс элемента иерархии
class Shape {
public:
virtual void accept(ShapeVisitor& visitor) = 0;
virtual ~Shape() = default;
};
// Конкретные классы иерархии
class Circle : public Shape {
public:
double radius;
Circle(double r) : radius(r) {}
void accept(ShapeVisitor& visitor) override { visitor.visit(*this); }
};
class Rectangle : public Shape {
public:
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
void accept(ShapeVisitor& visitor) override { visitor.visit(*this); }
};
// Конкретные посетители (новые операции)
class AreaCalculator : public ShapeVisitor {
public:
void visit(Circle& circle) override {
double area = 3.14159 * circle.radius * circle.radius;
std::cout << "Area of circle: " << area << 'n';
}
void visit(Rectangle& rect) override {
double area = rect.width * rect.height;
std::cout << "Area of rectangle: " << area << 'n';
}
};
class DrawVisitor : public ShapeVisitor {
public:
void visit(Circle& circle) override {
std::cout << "Drawing a circle with radius " << circle.radius << 'n';
}
void visit(Rectangle& rect) override {
std::cout << "Drawing a rectangle " << rect.width << "x" << rect.height << 'n';
}
};
int main() {
std::vector<Shape*> shapes = {new Circle(5.0), new Rectangle(3.0, 4.0)};
AreaCalculator areaCalc;
DrawRenderer drawRender;
for (auto shape : shapes) {
shape->accept(areaCalc); // Вычисляем площадь
shape->accept(drawRender); // Рисуем
}
// ... очистка памяти
return 0;
}
Когда использовать?
- Когда иерархия классов стабильна, но над ней нужно выполнять множество различных и часто меняющихся операций.
- Когда логика операции сильно зависит от конкретного типа класса.
Недостатки:
- Усложняет добавление новых классов в иерархию (придётся обновлять всех посетителей).
- Посетитель часто требует открытого доступа к полям класса, что может нарушать инкапсуляцию.
Ответ 18+ 🔞
А, слушай, вот это паттерн — просто ёперный театр! Представь себе: у тебя есть целая куча классов, иерархия, всё стабильно, не шелохнётся. А потом приходит какой-нибудь менеджер и говорит: «А давайте мы к этим вашим фигурам ещё и JSON экспорт добавим, и в XML, и валидацию, и статистику по цветам...». И ты такой сидишь и думаешь: «Ну ядрёна вошь, сейчас же во все эти классы лезть, в каждый метод дописывать, всё поломается!».
Вот тут-то и вырубается из-за угла Посетитель. Суть его, если по-простому, — отделить операции от объектов, над которыми они выполняются. То есть ты не лезешь в старые, проверенные классы, а выносишь каждую новую операцию в отдельный класс-«посетитель». А сами классы всего лишь один раз учатся говорить «да, я готов к посещению» через метод accept().
Как это работает, бля?
Это называется двойная диспетчеризация, звучит страшно, но на деле — хитрая жопа. Когда ты вызываешь shape->accept(visitor), фигура сама, своим внутренним чутьём, говорит: «Ага, я же круг! Значит, нужно вызвать у этого посетителя метод visit(*this), где *this — это я, конкретный круг». И посетитель уже знает, что делать именно с кругом. Получается, операция выбирается сразу по двум типам: и по типу фигуры, и по типу посетителя. Умно, чёрт возьми.
Смотри, как это в коде выглядит, на примере геометрических фигур:
#include <iostream>
#include <vector>
// Это наши будущие классы, объявляем заранее, чтобы Visitor о них знал
class Circle;
class Rectangle;
// А вот и сам царь и бог — интерфейс Посетителя.
// У него метод visit() для КАЖДОГО типа, который он может посетить.
class ShapeVisitor {
public:
virtual void visit(Circle& circle) = 0;
virtual void visit(Rectangle& rect) = 0;
virtual ~ShapeVisitor() = default;
};
// Базовый класс для всех фигур. Его главная и часто единственная новая обязанность.
class Shape {
public:
virtual void accept(ShapeVisitor& visitor) = 0; // "Входите, гости дорогие!"
virtual ~Shape() = default;
};
// Конкретные фигуры. Смотри, как они реализуют accept.
class Circle : public Shape {
public:
double radius;
Circle(double r) : radius(r) {}
// Вот тут магия! *this знает, что он Circle, и вызывает visit(*this).
void accept(ShapeVisitor& visitor) override { visitor.visit(*this); }
};
class Rectangle : public Shape {
public:
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
void accept(ShapeVisitor& visitor) override { visitor.visit(*this); }
};
// А теперь — НОВЫЕ ОПЕРАЦИИ, которые мы прикрутили, НЕ ЛЕЗЯ В КЛАССЫ ФИГУР!
// Посетитель №1: Калькулятор площади.
class AreaCalculator : public ShapeVisitor {
public:
void visit(Circle& circle) override {
double area = 3.14159 * circle.radius * circle.radius;
std::cout << "Площадь круга: " << area << 'n';
}
void visit(Rectangle& rect) override {
double area = rect.width * rect.height;
std::cout << "Площадь прямоугольника: " << area << 'n';
}
};
// Посетитель №2: Рендерер. Хочешь ещё экспорт в SVG? Пиши нового посетителя!
class DrawVisitor : public ShapeVisitor {
public:
void visit(Circle& circle) override {
std::cout << "Рисую круг радиусом " << circle.radius << 'n';
}
void visit(Rectangle& rect) override {
std::cout << "Рисую прямоугольник " << rect.width << "x" << rect.height << 'n';
}
};
int main() {
// Наш зоопарк фигур
std::vector<Shape*> shapes = {new Circle(5.0), new Rectangle(3.0, 4.0)};
AreaCalculator areaCalc;
DrawVisitor drawRender;
for (auto shape : shapes) {
shape->accept(areaCalc); // Считаем площадь для каждой
shape->accept(drawRender); // Рисуем каждую
// Хочешь новую операцию? Просто создай нового посетителя и accept его тут же!
}
// ... не забудь почистить за собой память, распиздяй!
return 0;
}
Так когда же это говно полезно?
- Когда твоя иерархия классов застыла как скала, но над ней нужно выполнять овердохуища разных операций, которые ещё и меняются каждую неделю.
- Когда логика операции дико зависит от конкретного типа. Например, рендерить круг и треугольник — это две абсолютно разные истории.
Но не обольщайся, подвохи есть:
- Добавил новый класс в иерархию? Приготовься обойти всех существующих посетителей и добавить в каждый метод
visit()для этого нового типа. Если посетителей много — это пиздец, а не работа. Доверия к такой системе — ноль ебать, если иерархия нестабильна. - Часто посетителю нужен доступ к внутренностям объекта (полям). А если они приватные? Придётся делать friend-объявления или нарушать инкапсуляцию, что тоже не айс.
Короче, паттерн мощный, но ты ходишь по охуенно тонкому льду. Используй, только когда действительно видишь, что операции плодятся, а классы — нет. Иначе получишь такую головную боль, что мало не покажется.