← Назад к вопросам

Что делает паттерн Visitor?

1.7 Middle🔥 151 комментариев
#ООП и проектирование#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Паттерн Visitor: назначение и применение

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

Основная идея

Вместо того чтобы писать операции внутри каждого класса иерархии, мы выносим логику операций в отдельные классы (Visitors). Это позволяет:

  • Соблюдать принцип Single Responsibility
  • Избежать множественных условных операторов
  • Легко добавлять новые операции без изменения существующего кода

Проблема, которую решает Visitor

// ❌ Плохо — логика операций размазана по классам
class Shape {
  virtual void draw() = 0;
  virtual void save() = 0;
  virtual void validate() = 0;
  virtual void serialize() = 0;
  // Каждая операция требует изменения всей иерархии!
};

class Circle : public Shape {
  void draw() override { /* ... */ }
  void save() override { /* ... */ }
  void validate() override { /* ... */ }
  void serialize() override { /* ... */ }
};

Решение с паттерном Visitor

// ✅ Хорошо — операции отделены от структуры
class Visitor {
public:
  virtual ~Visitor() = default;
  virtual void visit(Circle* circle) = 0;
  virtual void visit(Rectangle* rect) = 0;
  virtual void visit(Triangle* tri) = 0;
};

// Иерархия Figure остаётся простой и стабильной
class Figure {
public:
  virtual ~Figure() = default;
  virtual void accept(Visitor* visitor) = 0;  // Dual dispatch
};

class Circle : public Figure {
public:
  void accept(Visitor* visitor) override {
    visitor->visit(this);  // Вызывает visit(Circle*)
  }
  double radius = 5.0;
};

class Rectangle : public Figure {
public:
  void accept(Visitor* visitor) override {
    visitor->visit(this);  // Вызывает visit(Rectangle*)
  }
  double width = 10.0, height = 20.0;
};

Конкретные Visitors

// Visitor для отрисовки
class DrawVisitor : public Visitor {
public:
  void visit(Circle* circle) override {
    std::cout << "Drawing circle with radius " << circle->radius << std::endl;
  }
  void visit(Rectangle* rect) override {
    std::cout << "Drawing rectangle " << rect->width << "x" << rect->height << std::endl;
  }
  void visit(Triangle* tri) override {
    std::cout << "Drawing triangle" << std::endl;
  }
};

// Visitor для вычисления площади
class AreaVisitor : public Visitor {
public:
  double area = 0.0;
  
  void visit(Circle* circle) override {
    area = 3.14159 * circle->radius * circle->radius;
  }
  void visit(Rectangle* rect) override {
    area = rect->width * rect->height;
  }
  void visit(Triangle* tri) override {
    area = 0.0;  // Упрощённо
  }
};

// Visitor для сохранения в БД
class SerializeVisitor : public Visitor {
public:
  void visit(Circle* circle) override {
    database.insert("circles", {"radius", circle->radius});
  }
  void visit(Rectangle* rect) override {
    database.insert("rectangles", {"width", rect->width}, {"height", rect->height});
  }
  void visit(Triangle* tri) override {
    // Сохранение треугольника
  }
};

Использование

int main() {
  std::vector<std::unique_ptr<Figure>> figures;
  figures.push_back(std::make_unique<Circle>());
  figures.push_back(std::make_unique<Rectangle>());
  figures.push_back(std::make_unique<Triangle>());

  // Добавляем новую операцию БЕЗ изменения Figure, Circle и т.д.
  DrawVisitor draw_visitor;
  for (auto& fig : figures) {
    fig->accept(&draw_visitor);  // Полиморфизм!
  }

  // Ещё одна операция
  AreaVisitor area_visitor;
  for (auto& fig : figures) {
    fig->accept(&area_visitor);
  }
  std::cout << "Total area: " << area_visitor.area << std::endl;
}

Реальные примеры в production

1. Обход и валидация AST в компиляторе

class ASTVisitor {
public:
  virtual void visit(BinaryExprNode* node) = 0;
  virtual void visit(FunctionCallNode* node) = 0;
  virtual void visit(VariableNode* node) = 0;
};

class TypeCheckVisitor : public ASTVisitor {
  void visit(BinaryExprNode* node) override {
    // Проверить типы операндов
    validateTypes(node->left, node->right);
  }
};

class CodeGenVisitor : public ASTVisitor {
  void visit(BinaryExprNode* node) override {
    // Сгенерировать machine code
    emitAdd(node->left, node->right);
  }
};

2. Обработка различных типов XML элементов

class XMLElement {
public:
  virtual void accept(XMLVisitor* visitor) = 0;
};

class XMLTag : public XMLElement {
  void accept(XMLVisitor* visitor) override { visitor->visit(this); }
};

class XMLVisitor {
public:
  virtual void visit(XMLTag* tag) = 0;
  virtual void visit(XMLAttribute* attr) = 0;
  virtual void visit(XMLText* text) = 0;
};

Преимущества и недостатки

Преимущества

  • Open/Closed Principle — открыто для расширения (новые Visitors), закрыто для модификации (Figure, Circle)
  • Separation of Concerns — логика операции отделена от структуры данных
  • Легко добавлять операции — просто создаём новый Visitor
  • Централизация логики — вся логика одной операции в одном месте

Недостатки

  • Сложность — требует понимания double dispatch
  • Хрупкость иерархии — добавление новой фигуры требует обновления всех Visitors
  • Производительность — виртуальные вызовы имеют overhead
  • Скрывает логику — операции не очевидны из класса Figure

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

✅ Используйте когда:

  • Часто добавляются новые операции к существующей структуре
  • Операции логически независимы
  • Иерархия классов стабильна
  • Нужна типизированная обработка разных элементов

❌ Избегайте когда:

  • Иерархия часто меняется
  • Мало операций
  • Нужна максимальная производительность
  • Команда не знакома с паттерном

Альтернативы

  1. Если иерархия нестабильна — используйте полиморфные методы в классах
  2. Если нужны только 1-2 операции — просто добавьте методы
  3. В современном C++ — рассмотрите std::variant с std::visit (более типобезопасно)
Что делает паттерн Visitor? | PrepBro