Что такое паттерн проектирования «Посетитель» (Visitor)?

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

Ответ

Паттерн «Посетитель» — это поведенческий паттерн, который позволяет добавлять новые операции к объектам, не изменяя их классы. Он полезен, когда у вас есть сложная структура объектов (например, дерево AST — абстрактного синтаксического дерева) и вы хотите выполнять над ними разнородные операции (логирование, рендеринг, анализ).

Ключевая идея: Вы отделяете алгоритм от структуры объектов, на которых он работает. Новые операции добавляются путём создания нового класса «посетителя».

Основные компоненты:

  1. IVisitor — интерфейс с методами Visit для каждого типа элемента в структуре.
  2. IElement — интерфейс элемента, который принимает посетителя (метод Accept).
  3. Конкретные элементы (Concrete Elements) — реализуют Accept, вызывая соответствующий метод посетителя.
  4. Конкретные посетители (Concrete Visitors) — реализуют интерфейс IVisitor, определяя поведение для каждого типа элемента.

Пример на C#:

// Элементы
public interface IShape
{
    void Accept(IShapeVisitor visitor);
}

public class Circle : IShape
{
    public double Radius { get; set; }
    public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}

public class Rectangle : IShape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}

// Посетитель
public interface IShapeVisitor
{
    void Visit(Circle circle);
    void Visit(Rectangle rectangle);
}

// Конкретная операция — вычисление площади
public class AreaCalculator : IShapeVisitor
{
    public double TotalArea { get; private set; }
    public void Visit(Circle circle)
    {
        TotalArea += Math.PI * circle.Radius * circle.Radius;
    }
    public void Visit(Rectangle rectangle)
    {
        TotalArea += rectangle.Width * rectangle.Height;
    }
}

// Использование
List<IShape> shapes = new List<IShape> { new Circle { Radius = 5 }, new Rectangle { Width = 2, Height = 3 } };
var calculator = new AreaCalculator();
foreach (var shape in shapes)
{
    shape.Accept(calculator);
}
Console.WriteLine($"Total area: {calculator.TotalArea}");

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

  • Открыт/закрыт: Легко добавить новую операцию (нового посетителя), не трогая классы элементов.
  • Сбор родственных операций: Логика одной операции сосредоточена в одном классе посетителя.

Недостатки:

  • Сложно добавить новый тип элемента, так как придётся обновить всех существующих посетителей.
  • Посетитель может получить доступ к приватным полям элементов только если они ему «откроются» (например, через внутренний интерфейс).