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

«В чём суть паттерна проектирования «Посетитель» (Visitor)?» — вопрос из категории Паттерны, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

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

Когда использовать?

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

Недостатки:

  • Усложняет добавление новых классов в иерархию (придётся обновлять всех посетителей).
  • Посетитель часто требует открытого доступа к полям класса, что может нарушать инкапсуляцию.