Что это такое
Паттерн Посетитель (Visitor) добавляет новую операцию к семейству типов, не изменяя их. Типы один раз учатся одному — accept(visitor), — и с этого момента каждая новая операция — это просто новый посетитель с одним методом для каждого типа.
Он меняет привычный баланс: добавлять операции становится легко, а добавление типов становится дорогим изменением.
Пример: операции над фигурами
Фигуры больше не меняются. Вывод площадей — это один посетитель, вывод названий — другой, а посетитель для экспорта в SVG, добавленный завтра, не затронет ни Circle, ни Rectangle.
Новые операции, нетронутые типы
Оба посетителя обходят один и тот же список фигур через одну и ту же «дверь» accept. Каждый посетитель хранит всю операцию в одном месте, вместо того чтобы разбрасывать её по типам.
Вывод: полная программа на Python
from dataclasses import dataclass
from typing import Protocol
# Посетитель — это одна целая операция над всеми типами фигур.
class Visitor(Protocol):
def visit_circle(self, circle: "Circle") -> None: ...
def visit_rectangle(self, rectangle: "Rectangle") -> None: ...
@dataclass
class Circle:
radius: float
def accept(self, visitor: Visitor) -> None:
visitor.visit_circle(self)
@dataclass
class Rectangle:
width: float
height: float
def accept(self, visitor: Visitor) -> None:
visitor.visit_rectangle(self)
class AreaPrinter:
def visit_circle(self, circle: Circle) -> None:
print(f"площадь круга: {3.14159 * circle.radius * circle.radius:.2f}")
def visit_rectangle(self, rectangle: Rectangle) -> None:
print(f"площадь прямоугольника: {rectangle.width * rectangle.height:.2f}")
class NamePrinter:
def visit_circle(self, circle: Circle) -> None:
print("это круг")
def visit_rectangle(self, rectangle: Rectangle) -> None:
print("это прямоугольник")
shapes = [Circle(radius=2), Rectangle(width=3, height=4)]
for shape in shapes:
shape.accept(AreaPrinter())
for shape in shapes:
shape.accept(NamePrinter())
Когда его использовать
Используйте его, когда у стабильного семейства типов постоянно появляются новые операции — компиляторы, обходящие синтаксические деревья, документы, получающие новые экспортёры, фигуры, получающие средства отрисовки и измерения.
Не используйте его, когда семейство типов ещё растёт: каждый новый тип требует изменения в каждом посетителе. Во многих программах на Python оператор match по типам делает ту же работу с меньшим количеством кода — выбирайте посетителя, когда множатся операции, а не типы.
Плюсы и минусы
Плюсы: каждая операция целиком находится в своём классе; новые операции не требуют никаких изменений в посещаемых типах; связанная логика больше не разбросана по коду.
Минусы: двойная диспетчеризация (accept вызывает метод посетителя в ответ) сбивает с толку новичков; добавление одного посещаемого типа означает правку каждого посетителя; интерфейс посещаемого семейства фиксируется рано.