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

Ответ

Паттерн «Посетитель» — это поведенческий паттерн, который позволяет добавлять новые операции к объектам, не изменяя их классы. Он полезен, когда у вас есть сложная структура объектов (например, дерево 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}");

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

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

Недостатки:

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

Ответ 18+ 🔞

А, паттерн «Посетитель»! Ну это ж классика, блядь, как «Муму» у Тургенева — все знают, что грустно, но до конца не понимают, зачем он нужен, пока сами не наступите на эти грабли.

Смотри, представь себе: у тебя есть зоопарк объектов — ну там кружочки, квадратики, треугольники, всякая хуйня. И ты такой: «О, хочу посчитать площадь всей этой красоты!». А потом: «А теперь хочу вывести их в JSON!». А потом: «А щас я их всех в красный цвет покрашу!». И если для каждой такой операции лезть в каждый класс фигур и добавлять метод CalculateArea(), ToJson(), PaintRed() — это же пиздец, чувак. Классы распухнут, как жаба, и каждый раз их пересобирать.

Вот тут-то и выходит на сцену этот хитрожопый «Посетитель». Суть в том, чтобы вынести все эти операции наружу, в отдельные классы-посетители. А сами объекты должны научиться только одному — принимать этого посетителя, говорить ему: «На, я вот такой-то, делай со мной что хочешь».

Как это выглядит в коде, блядь:

  1. Интерфейс посетителя (IVisitor). Это как список дел, которые гость может сделать с каждым типом хозяина. Для круга — одно, для прямоугольника — другое.
  2. Интерфейс элемента (IElement). Это наша фигура. У неё один ключевой метод — Accept(IVisitor v). Всё, что она делает — говорит: «Эй, посетитель, вот я кто, работай!».
  3. Конкретные элементы (Circle, Rectangle). Реализуют этот Accept самым тупым образом: просто вызывают у посетителя метод Visit(this). Вся магия — не в них.
  4. Конкретные посетители (AreaCalculator, JsonExporter). Вот где вся соль! Это отдельные классы, которые знают, как именно работать с каждым типом фигуры. Хочешь новую операцию? Создаёшь нового посетителя, и ни один класс фигур даже не чихнёт.

Пример, как это в жизни:

// Вот наши упоротые фигурки. Они умеют только принимать гостей.
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 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}"); // Считает всё правильно!

Плюсы, блядь, очевидны:

  • Не лезем в старые классы. Захотел новую операцию (например, CheckCollision) — написал нового посетителя CollisionDetector, и ни одна старая фигура даже не узнает об этом. Принцип «открыт для расширений, закрыт для изменений» в действии, красота!
  • Вся логика операции в одном месте. Не раскидана по всем классам, а аккуратно упакована в одном классе посетителя. Хочешь понять, как считается площадь? Открывай AreaCalculator и читай.

Но и минусы, сука, есть, куда без них:

  • Добавить новый тип элемента — пиздец. Захотел добавить Triangle? Придётся бегать по всем существующим посетителям (AreaCalculator, JsonExporter и т.д.) и в каждом дописывать метод Visit(Triangle triangle). Если посетителей много — это адская работа.
  • Доступ к приватным полям. Посетитель живёт снаружи класса, поэтому если ему нужны внутренние данные объекта, классу придётся их «выставить» (через публичные свойства или какой-нибудь internal интерфейс). Не всегда удобно.

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