Что это такое
Паттерн Visitor («Посетитель») добавляет новую операцию к семейству типов, не изменяя сами типы. Типы один раз узнают только одно — Accept(visitor) — и с этого момента каждая новая операция становится просто новым посетителем с одним методом на каждый тип.
Он переворачивает привычный компромисс: добавлять операции становится легко, а добавлять типы — дорого.
Пример: операции над фигурами
Фигуры больше никогда не меняются. Вывод площадей — это один посетитель, вывод названий — другой, а завтрашний посетитель для экспорта в SVG не затронет ни Circle, ни Rectangle.
Новые операции, нетронутые типы
Оба посетителя проходят по одному и тому же списку фигур через одну и ту же «дверь» Accept. Каждый посетитель хранит всю операцию в одном месте, вместо того чтобы разбрасывать её по типам.
Вывод: программа на Go и захваченный вывод
package main
import "fmt"
type Shape interface {
Accept(v Visitor)
}
type Circle struct {
Radius float64
}
func (c Circle) Accept(v Visitor) { v.VisitCircle(c) }
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Accept(v Visitor) { v.VisitRectangle(r) }
// Посетитель — это одна целая операция над всеми типами фигур.
type Visitor interface {
VisitCircle(c Circle)
VisitRectangle(r Rectangle)
}
type AreaPrinter struct{}
func (AreaPrinter) VisitCircle(c Circle) {
fmt.Printf("площадь круга: %.2f\n", 3.14159*c.Radius*c.Radius)
}
func (AreaPrinter) VisitRectangle(r Rectangle) {
fmt.Printf("площадь прямоугольника: %.2f\n", r.Width*r.Height)
}
type NamePrinter struct{}
func (NamePrinter) VisitCircle(c Circle) { fmt.Println("это круг") }
func (NamePrinter) VisitRectangle(r Rectangle) { fmt.Println("это прямоугольник") }
func main() {
shapes := []Shape{Circle{Radius: 2}, Rectangle{Width: 3, Height: 4}}
for _, s := range shapes {
s.Accept(AreaPrinter{})
}
for _, s := range shapes {
s.Accept(NamePrinter{})
}
}
Когда его использовать
Используйте его, когда стабильное семейство типов постоянно обрастает новыми операциями: компиляторы обходят синтаксические деревья, документы получают новые экспортёры, фигуры получают новые способы отрисовки и измерения.
Откажитесь от него, когда семейство типов ещё растёт: каждый новый тип заставляет менять каждого посетителя. Во многих программах на Go ту же работу выполняет type switch с меньшим количеством механики — берите посетителя тогда, когда множатся операции, а не типы.
Плюсы и минусы
Плюсы: целые операции живут каждая в своём типе; новые операции не требуют никаких изменений в посещаемых типах; связанная логика перестаёт быть разбросанной.
Минусы: двойная диспетчеризация (Accept, вызывающий обратный вызов) сбивает новичков с толку; добавление одного посещаемого типа означает правку каждого посетителя; интерфейс посещаемого семейства фиксируется слишком рано.