Visitor (Посетитель)
Проблема
Есть сложная структура данных (AST, документ, дерево) с разными типами узлов. Нужно добавлять новые операции над этими узлами, не изменяя их код. Visitor отделяет алгоритм от структуры, по которой он работает.
Важно: В Go Visitor реализуется проще, чем в Java, благодаря type switch. Классический double dispatch нужен редко.
Диаграмма
Visitor Node
+-------------------+ +-------------------+
| VisitNumber(n) | | Accept(Visitor) |
| VisitString(s) | +-------------------+
| VisitBinary(b) | ^
+-------------------+ +----+----+
^ | | |
+----+----+ Number String Binary
| |
Evaluator Printer
Подход 1: Type Switch (Go-идиоматичный)
Самый простой и рекомендуемый подход для Go:
package ast
import "fmt"
// Node is any node in the AST.
type Node interface {
node() // unexported marker method
}
// --- Concrete nodes ---
type NumberLit struct {
Value float64
}
func (*NumberLit) node() {}
type StringLit struct {
Value string
}
func (*StringLit) node() {}
type BinaryExpr struct {
Left Node
Op string
Right Node
}
func (*BinaryExpr) node() {}
type UnaryExpr struct {
Op string
Operand Node
}
func (*UnaryExpr) node() {}
type CallExpr struct {
Name string
Args []Node
}
func (*CallExpr) node() {}
// --- Operations via type switch ---
// Eval evaluates an AST node.
func Eval(n Node) (float64, error) {
switch v := n.(type) {
case *NumberLit:
return v.Value, nil
case *BinaryExpr:
left, err := Eval(v.Left)
if err != nil {
return 0, err
}
right, err := Eval(v.Right)
if err != nil {
return 0, err
}
switch v.Op {
case "+":
return left + right, nil
case "-":
return left - right, nil
case "*":
return left * right, nil
case "/":
if right == 0 {
return 0, fmt.Errorf("division by zero")
}
return left / right, nil
default:
return 0, fmt.Errorf("unknown operator: %s", v.Op)
}
case *UnaryExpr:
val, err := Eval(v.Operand)
if err != nil {
return 0, err
}
switch v.Op {
case "-":
return -val, nil
default:
return 0, fmt.Errorf("unknown unary operator: %s", v.Op)
}
default:
return 0, fmt.Errorf("cannot evaluate node type %T", n)
}
}
// Print returns a string representation of the AST.
func Print(n Node) string {
switch v := n.(type) {
case *NumberLit:
return fmt.Sprintf("%.g", v.Value)
case *StringLit:
return fmt.Sprintf("%q", v.Value)
case *BinaryExpr:
return fmt.Sprintf("(%s %s %s)", Print(v.Left), v.Op, Print(v.Right))
case *UnaryExpr:
return fmt.Sprintf("(%s%s)", v.Op, Print(v.Operand))
case *CallExpr:
args := make([]string, len(v.Args))
for i, a := range v.Args {
args[i] = Print(a)
}
return fmt.Sprintf("%s(%s)", v.Name, strings.Join(args, ", "))
default:
return fmt.Sprintf("<%T>", n)
}
}
Использование:
func main() {
// Represents: (2 + 3) * 4
expr := &ast.BinaryExpr{
Left: &ast.BinaryExpr{
Left: &ast.NumberLit{Value: 2},
Op: "+",
Right: &ast.NumberLit{Value: 3},
},
Op: "*",
Right: &ast.NumberLit{Value: 4},
}
fmt.Println(ast.Print(expr)) // ((2 + 3) * 4)
result, _ := ast.Eval(expr)
fmt.Println(result) // 20
}
Подход 2: Классический Visitor с Accept
Когда типы узлов определяются в другом пакете и вы хотите расширять операции без изменения существующих файлов:
package document
// Visitor defines operations for each node type.
type Visitor interface {
VisitParagraph(p *Paragraph)
VisitHeading(h *Heading)
VisitImage(img *Image)
VisitTable(t *Table)
VisitCodeBlock(c *CodeBlock)
}
// Element is a document node that accepts visitors.
type Element interface {
Accept(v Visitor)
}
// --- Concrete elements ---
type Paragraph struct {
Text string
}
func (p *Paragraph) Accept(v Visitor) { v.VisitParagraph(p) }
type Heading struct {
Level int
Text string
}
func (h *Heading) Accept(v Visitor) { v.VisitHeading(h) }
type Image struct {
URL string
Alt string
}
func (img *Image) Accept(v Visitor) { v.VisitImage(img) }
type Table struct {
Headers []string
Rows [][]string
}
func (t *Table) Accept(v Visitor) { v.VisitTable(t) }
type CodeBlock struct {
Language string
Code string
}
func (c *CodeBlock) Accept(v Visitor) { v.VisitCodeBlock(c) }
// Document is a collection of elements.
type Document struct {
Elements []Element
}
func (d *Document) Accept(v Visitor) {
for _, elem := range d.Elements {
elem.Accept(v)
}
}
Конкретные посетители
package export
import (
"fmt"
"strings"
"myapp/document"
)
// HTMLExporter converts a document to HTML.
type HTMLExporter struct {
builder strings.Builder
}
func (e *HTMLExporter) VisitParagraph(p *document.Paragraph) {
fmt.Fprintf(&e.builder, "<p>%s</p>\n", p.Text)
}
func (e *HTMLExporter) VisitHeading(h *document.Heading) {
fmt.Fprintf(&e.builder, "<h%d>%s</h%d>\n", h.Level, h.Text, h.Level)
}
func (e *HTMLExporter) VisitImage(img *document.Image) {
fmt.Fprintf(&e.builder, `<img src="%s" alt="%s" />`+"\n", img.URL, img.Alt)
}
func (e *HTMLExporter) VisitTable(t *document.Table) {
e.builder.WriteString("<table>\n<tr>")
for _, h := range t.Headers {
fmt.Fprintf(&e.builder, "<th>%s</th>", h)
}
e.builder.WriteString("</tr>\n")
for _, row := range t.Rows {
e.builder.WriteString("<tr>")
for _, cell := range row {
fmt.Fprintf(&e.builder, "<td>%s</td>", cell)
}
e.builder.WriteString("</tr>\n")
}
e.builder.WriteString("</table>\n")
}
func (e *HTMLExporter) VisitCodeBlock(c *document.CodeBlock) {
fmt.Fprintf(&e.builder, "<pre><code class=\"%s\">%s</code></pre>\n", c.Language, c.Code)
}
func (e *HTMLExporter) Result() string {
return e.builder.String()
}
// MarkdownExporter converts a document to Markdown.
type MarkdownExporter struct {
builder strings.Builder
}
func (e *MarkdownExporter) VisitParagraph(p *document.Paragraph) {
fmt.Fprintf(&e.builder, "%s\n\n", p.Text)
}
func (e *MarkdownExporter) VisitHeading(h *document.Heading) {
fmt.Fprintf(&e.builder, "%s %s\n\n", strings.Repeat("#", h.Level), h.Text)
}
func (e *MarkdownExporter) VisitImage(img *document.Image) {
fmt.Fprintf(&e.builder, "\n\n", img.Alt, img.URL)
}
func (e *MarkdownExporter) VisitTable(t *document.Table) {
// Header
e.builder.WriteString("| " + strings.Join(t.Headers, " | ") + " |\n")
e.builder.WriteString("|" + strings.Repeat(" --- |", len(t.Headers)) + "\n")
for _, row := range t.Rows {
e.builder.WriteString("| " + strings.Join(row, " | ") + " |\n")
}
e.builder.WriteString("\n")
}
func (e *MarkdownExporter) VisitCodeBlock(c *document.CodeBlock) {
fmt.Fprintf(&e.builder, "```%s\n%s\n```\n\n", c.Language, c.Code)
}
func (e *MarkdownExporter) Result() string {
return e.builder.String()
}
Использование
func main() {
doc := &document.Document{
Elements: []document.Element{
&document.Heading{Level: 1, Text: "Hello Go"},
&document.Paragraph{Text: "Go is a great language."},
&document.CodeBlock{Language: "go", Code: `fmt.Println("Hello!")`},
},
}
// Export to HTML
html := &export.HTMLExporter{}
doc.Accept(html)
fmt.Println(html.Result())
// Export to Markdown -- new operation, NO changes to document package
md := &export.MarkdownExporter{}
doc.Accept(md)
fmt.Println(md.Result())
}
Type Switch vs Classic Visitor
| Критерий | Type Switch | Classic Visitor |
|---|---|---|
| Добавить операцию | Новая функция (просто) | Новый Visitor (просто) |
| Добавить тип узла | Обновить все switch (сложно) | Обновить Visitor интерфейс (сложно) |
| Проверка полноты | Нет (runtime) | Да (compile-time) |
| Идиоматичность Go | Высокая | Средняя |
| Лучше когда | Типы стабильны, операции меняются | Операции стабильны, типы стабильны |
Когда использовать
Используйте, когда:
- Сложная структура данных с разными типами узлов (AST, DOM)
- Нужно добавлять новые операции без изменения узлов
- Набор типов узлов стабилен (редко добавляются)
- Экспорт в разные форматы (HTML, PDF, Markdown)
Не используйте, когда:
- Типы узлов часто добавляются (нужно обновлять Visitor)
- Простая структура (type switch достаточен)
- Одна операция (нет смысла в абстракции)
Сравнение с похожими паттернами
| Паттерн | Отличие |
|---|---|
| Strategy | Выбор одного алгоритма; Visitor -- разные операции для разных типов |
| Iterator | Обходит элементы; Visitor выполняет операции при обходе |
| Composite | Определяет структуру дерева; Visitor добавляет операции к дереву |