В чём суть паттерна проектирования «Посетитель» (Visitor)?

Ответ

Посетитель — это поведенческий паттерн, который позволяет добавлять новые операции к целым иерархиям классов, не изменяя сами эти классы. Он реализует принцип двойной диспетчеризации: операция выполняется в зависимости от типов как самого посетителя, так и посещаемого элемента.

Ключевая идея: Вы выносите набор родственных операций (например, рендеринг, экспорт, валидация) в отдельные классы-посетители. Каждый класс иерархии должен всего лишь один раз реализовать метод 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-объявления или нарушать инкапсуляцию, что тоже не айс.

Короче, паттерн мощный, но ты ходишь по охуенно тонкому льду. Используй, только когда действительно видишь, что операции плодятся, а классы — нет. Иначе получишь такую головную боль, что мало не покажется.