Что это такое
Паттерн Visitor (Посетитель) добавляет новую операцию к семейству классов, не изменяя их. Классы один раз учатся одному — accept(visitor) — и с этого момента каждая новая операция это просто новый посетитель с одним методом на каждый класс.
Он переворачивает обычный компромисс: добавлять операции становится легко, а добавлять классы становится дорогим изменением.
Пример: операции над фигурами
Фигуры больше никогда не меняются. Печать площадей это один посетитель, печать названий другой — а завтрашний посетитель для экспорта в SVG не затронет ни Circle, ни Rectangle.
Новые операции, нетронутые классы
Оба посетителя проходят по одному и тому же массиву фигур через одну и ту же дверь accept: первый печатает площадь круга: 12.57 и площадь прямоугольника: 12.00, второй печатает два названия. Каждый посетитель хранит всю операцию в одном месте, вместо того чтобы разбрасывать её по классам.
Вывод: программа на TypeScript и захваченный вывод
interface Shape {
accept(visitor: ShapeVisitor): void
}
// Посетитель — это одна целая операция над всеми классами фигур.
interface ShapeVisitor {
visitCircle(circle: Circle): void
visitRectangle(rectangle: Rectangle): void
}
class Circle implements Shape {
readonly radius: number
constructor(radius: number) {
this.radius = radius
}
accept(visitor: ShapeVisitor): void {
visitor.visitCircle(this)
}
}
class Rectangle implements Shape {
readonly width: number
readonly height: number
constructor(width: number, height: number) {
this.width = width
this.height = height
}
accept(visitor: ShapeVisitor): void {
visitor.visitRectangle(this)
}
}
class AreaPrinter implements ShapeVisitor {
visitCircle(circle: Circle): void {
console.log("площадь круга:", (Math.PI * circle.radius * circle.radius).toFixed(2))
}
visitRectangle(rectangle: Rectangle): void {
console.log("площадь прямоугольника:", (rectangle.width * rectangle.height).toFixed(2))
}
}
class NamePrinter implements ShapeVisitor {
visitCircle(circle: Circle): void {
console.log("круг")
}
visitRectangle(rectangle: Rectangle): void {
console.log("прямоугольник")
}
}
const shapes: Shape[] = [new Circle(2), new Rectangle(3, 4)]
for (const shape of shapes) {
shape.accept(new AreaPrinter())
}
for (const shape of shapes) {
shape.accept(new NamePrinter())
}
Когда его использовать
Используйте его, когда стабильное семейство классов постоянно обрастает новыми операциями — компиляторы обходят синтаксические деревья, документы получают экспортёры, фигуры получают отрисовщики и измерители.
Откажитесь от него, когда семейство всё ещё растёт: каждый новый класс вынуждает менять каждого посетителя. Во многих программах на TypeScript switch по размеченному объединению делает ту же работу с меньшим количеством механики — берите посетителя тогда, когда умножаются операции, а не классы.
Плюсы и минусы
Плюсы: целые операции живут каждая в своём классе; новые операции не требуют никаких изменений в посещаемых классах; связанная логика перестаёт быть разбросанной.
Минусы: двойная диспетчеризация (accept, вызывающий обратно) сбивает с толку новичков; добавление одного посещаемого класса означает правку каждого посетителя; интерфейс посещаемого семейства фиксируется рано.