Что это такое
Паттерн Visitor (Посетитель) добавляет новую операцию к семейству классов, не меняя их. Классы один раз усваивают одно — accept($visitor) — и с этого момента каждая новая операция становится просто новым посетителем с одним методом на каждый класс.
Он меняет обычный компромисс: добавлять операции становится легко, а добавлять классы — дорого.
Пример: операции над фигурами
Фигуры больше никогда не меняются. Печать площадей — один посетитель, вывод названий — другой, а завтрашний посетитель для экспорта в SVG не затронет ни Circle, ни Rectangle.
Новые операции, неизменные классы
Оба посетителя проходят один и тот же список фигур через одну и ту же дверь accept. Каждый посетитель держит всю операцию в одном месте, вместо того чтобы разбрасывать её по классам.
Вывод: PHP-скрипт
<?php
interface Shape {
public function accept(Visitor $v): void;
}
class Circle implements Shape {
public function __construct(public float $radius) {}
public function accept(Visitor $v): void {
$v->visitCircle($this);
}
}
class Rectangle implements Shape {
public function __construct(
public float $width,
public float $height,
) {}
public function accept(Visitor $v): void {
$v->visitRectangle($this);
}
}
// Посетитель — это одна целая операция над всеми классами фигур.
interface Visitor {
public function visitCircle(Circle $c): void;
public function visitRectangle(Rectangle $r): void;
}
class AreaPrinter implements Visitor {
public function visitCircle(Circle $c): void {
printf("площадь круга: %.2f\n", 3.14159 * $c->radius * $c->radius);
}
public function visitRectangle(Rectangle $r): void {
printf("площадь прямоугольника: %.2f\n", $r->width * $r->height);
}
}
class NamePrinter implements Visitor {
public function visitCircle(Circle $c): void {
echo "круг\n";
}
public function visitRectangle(Rectangle $r): void {
echo "прямоугольник\n";
}
}
$shapes = [new Circle(2), new Rectangle(3, 4)];
foreach ($shapes as $s) {
$s->accept(new AreaPrinter());
}
foreach ($shapes as $s) {
$s->accept(new NamePrinter());
}
Когда его применять
Применяйте, когда стабильное семейство классов постоянно обрастает новыми операциями — компиляторы, обходящие синтаксические деревья, документы, получающие экспортёры, фигуры, получающие отрисовщики и измерители.
Откажитесь, когда семейство классов ещё растёт: каждый новый класс заставляет менять каждого посетителя. Часто проверка instanceof или match по $shape::class делает ту же работу с меньшим количеством механики — берите посетителя, когда размножаются операции, а не классы.
Плюсы и минусы
Плюсы: целые операции живут каждая в одном классе; новые операции не требуют никаких изменений в посещаемых классах; связанная логика перестаёт быть разбросанной.
Минусы: двойная диспетчеризация (accept, вызывающий обратно) сбивает новичков с толку; добавление одного посещаемого класса означает правку каждого посетителя; интерфейс посещаемого семейства замораживается рано.