Visitor (Посетитель)¶
Категория: паттерн поведения.
Проблема¶
Есть стабильная иерархия классов (набор видов фигур, узлов AST, элементов документа), но со временем нужно добавлять всё новые операции над этой иерархией (посчитать площадь, отрисовать, экспортировать, провалидировать). Если добавлять каждую операцию как метод прямо в базовый класс и его наследников, то: - классы иерархии разрастаются операциями, которые концептуально им не принадлежат (фигура ничего не должна "знать" про экспорт в конкретный формат); - при каждой новой операции приходится трогать и перекомпилировать все классы иерархии, даже если сама иерархия не менялась.
Решение¶
- Для иерархии вводится метод
Accept(visitor), который есть у каждого класса и просто передаётthisв подходящий метод посетителя (visitor.Visit(this)). - Определяется интерфейс посетителя с отдельным методом
Visit(...)на каждый конкретный тип из иерархии. - Каждая новая операция оформляется отдельным классом, реализующим интерфейс посетителя - без изменения классов самой иерархии.
- Благодаря
Acceptвызывается перегрузкаVisit, соответствующая реальному типу объекта (двойная диспетчеризация), поэтому не требуется приведение типов (as/is) внутри операций.
Структура¶
Element- интерфейс элементов иерархии с методомAccept(IVisitor visitor).ConcreteElementA/B- конкретные элементы, каждый передаёт себя вVisitor.Visit(this).Visitor- интерфейс с перегруженным методомVisitна каждый конкретный элемент.ConcreteVisitor- конкретная операция над иерархией.
Двойная диспетчеризация¶
Обычный вызов метода в C# выбирает перегрузку по статическому (объявленному) типу параметра. Прямой вызов visitor.Visit(shape), где shape объявлен как IShape, выбрал бы единственную подходящую по статическому типу перегрузку. Приём Accept решает эту проблему: внутри Circle.Accept компилятор уже точно знает, что this имеет тип Circle (а не IShape), поэтому вызов visitor.Visit(this) разрешается в перегрузку именно для Circle. Такой двухшаговый вызов (Accept → Visit) и называют двойной диспетчеризацией.
Когда применять¶
- Иерархия классов стабильна (новые виды элементов добавляются редко), а операции над ней добавляются часто.
- Нужно выполнять разные, не связанные друг с другом операции над одной и той же структурой объектов, не засоряя сами классы этой логикой.
- Нужно избавиться от цепочек
if (x is TypeA) ... else if (x is TypeB) ...при обработке разных типов из иерархии.
Плюсы¶
- Новая операция добавляется новым классом-посетителем, без изменения существующих классов иерархии (открытость/закрытость по отношению к операциям).
- Связанная логика одной операции над всеми типами иерархии сосредоточена в одном классе, а не размазана по методам разных классов.
Минусы¶
- Обратная сторона медали: добавить новый вид элемента в иерархию тяжело - нужно добавить метод
Visitв интерфейс посетителя и реализовать его во всех существующих посетителях. Visitor - паттерн для стабильных иерархий с изменчивым набором операций, а не наоборот. - Усложняет структуру кода (два интерфейса, двойная диспетчеризация) - оправдано только при реальном разнообразии операций.
- Посетителю иногда нужен доступ к внутреннему состоянию элемента, которое не хочется делать полностью публичным - это может подталкивать к ослаблению инкапсуляции.
Пример в .NET Framework / BCL¶
ExpressionVisitorвSystem.Linq.Expressions- классический учебный пример прямо из BCL: позволяет обходить и трансформировать деревья выражений LINQ, добавляя новые операции (например, перевод в SQL) без изменения классовExpression.- Разбор синтаксических деревьев в Roslyn (
SyntaxVisitor,CSharpSyntaxVisitor) - применяется для анализа и рефакторинга кода C#.