Visitor Pattern #

Imagine an Abstract Syntax Tree (AST) representing program code — made up of nodes like NumberLiteral, BinaryExpression, FunctionCall, and VariableReference. You need to perform several different operations on the same AST: evaluation to run the code, pretty-printing to display the code, type-checking for validation, and optimization to simplify expressions. Without the Visitor Pattern, every new operation has to be added as a new method on every node — modifying NumberLiteral, BinaryExpression, FunctionCall, and so on. With the Visitor Pattern, every operation becomes one self-contained Visitor class; the nodes do not need to change at all. Adding a new operation = adding one new Visitor.

What Is the Visitor Pattern? #

The Visitor Pattern is a behavioral design pattern that lets you add new operations to an existing object structure without modifying the classes within that structure. The Visitor separates the algorithm from the object structure it operates on — the object structure stays stable, and operations can be added freely.

In Go, which has no method overloading, the Visitor is implemented through double dispatch — two method calls working together to ensure the correct implementation is invoked based on the concrete types of two objects at once.

Two key properties of the Visitor Pattern:

  • Open for extension, closed for modification — adding a new operation does not modify the existing object structure
  • Centralized operations — all logic for one operation lives in one Visitor, not scattered across many classes
flowchart LR
    subgraph "Without Visitor — Operations Scattered"
        NL["NumberLiteral\\n+ Evaluate()\\n+ Print()\\n+ TypeCheck()\\n+ Optimize()"]
        BE["BinaryExpr\\n+ Evaluate()\\n+ Print()\\n+ TypeCheck()\\n+ Optimize()"]
        note1["Adding a new operation\\n= modify ALL nodes"]
    end

    subgraph "With Visitor — Operations Centralized"
        N2["NumberLiteral\\n+ Accept(visitor)"]
        B2["BinaryExpr\\n+ Accept(visitor)"]
        EV["EvaluateVisitor\\n+ VisitNumber()\\n+ VisitBinary()"]
        PR["PrintVisitor\\n+ VisitNumber()\\n+ VisitBinary()"]
        note2["Adding a new operation\\n= add 1 new Visitor\\nNodes unchanged"]
        N2 & B2 -->|Accept| EV & PR
    end

Double Dispatch: The Core Mechanism of Visitor #

In languages with method overloading (Java, C++), dispatch based on runtime type happens automatically. Go does not have this, so we use double dispatch explicitly.

// Without double dispatch — the concrete type is lost after entering the interface
func processNode(node Node, visitor Visitor) {
    // visitor.Visit(node) — cannot know whether node is a NumberLiteral or BinaryExpr!
}

// Double dispatch — two "dispatches" working together:
// Dispatch 1: node.Accept(visitor) — based on the node's concrete type
// Dispatch 2: visitor.VisitNumber(n) — based on the visitor's concrete type

type Node interface {
    Accept(visitor Visitor) // every node knows how to "accept" a visitor
}

type NumberLiteral struct{ Value float64 }

func (n *NumberLiteral) Accept(visitor Visitor) {
    visitor.VisitNumber(n) // dispatch to the right method based on the node type
}

type BinaryExpr struct {
    Left, Right Node
    Operator    string
}

func (b *BinaryExpr) Accept(visitor Visitor) {
    visitor.VisitBinary(b) // dispatch differs from NumberLiteral
}

// The Visitor knows which method is called based on the node type
type Visitor interface {
    VisitNumber(n *NumberLiteral)
    VisitBinary(b *BinaryExpr)
}
The double dispatch flow:

sequenceDiagram
    participant C as Client
    participant N as NumberLiteral
    participant V as EvaluateVisitor

    C->>N: Accept(evaluateVisitor)
    Note over N: Dispatch 1: the node knows its own type
    N->>V: VisitNumber(self)
    Note over V: Dispatch 2: the visitor knows how to handle NumberLiteral
    V-->>C: result

Full Implementation: AST Expression Processor #

Element Hierarchy (Nodes That Never Change) #

package ast

import "fmt"

// Node is the interface implemented by every AST node.
type Node interface {
    Accept(visitor Visitor) interface{}
    String() string
}

// NumberLiteral represents a constant number.
type NumberLiteral struct {
    Value float64
}

func (n *NumberLiteral) Accept(visitor Visitor) interface{} {
    return visitor.VisitNumber(n)
}

func (n *NumberLiteral) String() string {
    if n.Value == float64(int(n.Value)) {
        return fmt.Sprintf("%.0f", n.Value)
    }
    return fmt.Sprintf("%g", n.Value)
}

// StringLiteral represents a constant string.
type StringLiteral struct {
    Value string
}

func (s *StringLiteral) Accept(visitor Visitor) interface{} {
    return visitor.VisitString(s)
}

func (s *StringLiteral) String() string { return fmt.Sprintf("%q", s.Value) }

// BinaryExpression represents a binary operation (a + b, a * b, etc.).
type BinaryExpression struct {
    Left     Node
    Operator string // "+", "-", "*", "/", "=="
    Right    Node
}

func (b *BinaryExpression) Accept(visitor Visitor) interface{} {
    return visitor.VisitBinary(b)
}

func (b *BinaryExpression) String() string {
    return fmt.Sprintf("(%s %s %s)", b.Left.String(), b.Operator, b.Right.String())
}

// UnaryExpression represents a unary operation (-x, !x).
type UnaryExpression struct {
    Operator string
    Operand  Node
}

func (u *UnaryExpression) Accept(visitor Visitor) interface{} {
    return visitor.VisitUnary(u)
}

func (u *UnaryExpression) String() string {
    return fmt.Sprintf("(%s%s)", u.Operator, u.Operand.String())
}

// FunctionCall represents a function invocation.
type FunctionCall struct {
    FunctionName string
    Arguments    []Node
}

func (f *FunctionCall) Accept(visitor Visitor) interface{} {
    return visitor.VisitFunctionCall(f)
}

func (f *FunctionCall) String() string {
    args := make([]string, len(f.Arguments))
    for i, arg := range f.Arguments {
        args[i] = arg.String()
    }
    return fmt.Sprintf("%s(%s)", f.FunctionName, joinStrings(args, ", "))
}

func joinStrings(strs []string, sep string) string {
    result := ""
    for i, s := range strs {
        if i > 0 {
            result += sep
        }
        result += s
    }
    return result
}

Visitor Interface #

// Visitor defines the operation for each node type.
// Every new Visitor implemented adds a new operation
// without modifying any of the nodes above.
type Visitor interface {
    VisitNumber(n *NumberLiteral) interface{}
    VisitString(s *StringLiteral) interface{}
    VisitBinary(b *BinaryExpression) interface{}
    VisitUnary(u *UnaryExpression) interface{}
    VisitFunctionCall(f *FunctionCall) interface{}
}

Concrete Visitor 1: EvaluateVisitor #

package ast

import (
    "fmt"
    "math"
)

// EvaluateVisitor evaluates expressions and returns a numeric or string value.
// This is a new operation added WITHOUT modifying any node.
type EvaluateVisitor struct {
    variables map[string]interface{} // for variable resolution
}

func NewEvaluateVisitor(vars map[string]interface{}) *EvaluateVisitor {
    if vars == nil {
        vars = make(map[string]interface{})
    }
    return &EvaluateVisitor{variables: vars}
}

func (v *EvaluateVisitor) VisitNumber(n *NumberLiteral) interface{} {
    return n.Value
}

func (v *EvaluateVisitor) VisitString(s *StringLiteral) interface{} {
    return s.Value
}

func (v *EvaluateVisitor) VisitBinary(b *BinaryExpression) interface{} {
    left := b.Left.Accept(v)
    right := b.Right.Accept(v)

    // Numeric arithmetic
    leftNum, leftOK := toFloat(left)
    rightNum, rightOK := toFloat(right)

    if leftOK && rightOK {
        switch b.Operator {
        case "+":
            return leftNum + rightNum
        case "-":
            return leftNum - rightNum
        case "*":
            return leftNum * rightNum
        case "/":
            if rightNum == 0 {
                panic("division by zero")
            }
            return leftNum / rightNum
        case "==":
            return leftNum == rightNum
        case "<":
            return leftNum < rightNum
        case ">":
            return leftNum > rightNum
        }
    }

    // String concatenation
    if b.Operator == "+" {
        return fmt.Sprintf("%v%v", left, right)
    }

    return fmt.Errorf("unsupported operation: %v %s %v", left, b.Operator, right)
}

func (v *EvaluateVisitor) VisitUnary(u *UnaryExpression) interface{} {
    operand := u.Operand.Accept(v)
    switch u.Operator {
    case "-":
        if num, ok := toFloat(operand); ok {
            return -num
        }
    case "!":
        if b, ok := operand.(bool); ok {
            return !b
        }
    }
    return fmt.Errorf("unsupported unary: %s%v", u.Operator, operand)
}

func (v *EvaluateVisitor) VisitFunctionCall(f *FunctionCall) interface{} {
    args := make([]interface{}, len(f.Arguments))
    for i, arg := range f.Arguments {
        args[i] = arg.Accept(v)
    }

    switch f.FunctionName {
    case "abs":
        if len(args) == 1 {
            if num, ok := toFloat(args[0]); ok {
                return math.Abs(num)
            }
        }
    case "max":
        if len(args) == 2 {
            a, aOK := toFloat(args[0])
            b, bOK := toFloat(args[1])
            if aOK && bOK {
                if a > b {
                    return a
                }
                return b
            }
        }
    case "len":
        if len(args) == 1 {
            if s, ok := args[0].(string); ok {
                return float64(len(s))
            }
        }
    }
    return fmt.Errorf("unknown function: %s", f.FunctionName)
}

func toFloat(v interface{}) (float64, bool) {
    switch val := v.(type) {
    case float64:
        return val, true
    case int:
        return float64(val), true
    }
    return 0, false
}

Concrete Visitor 2: PrintVisitor #

package ast

import (
    "fmt"
    "strings"
)

// PrintVisitor produces a readable string representation of the AST.
// This new operation is added without modifying a single node.
type PrintVisitor struct {
    indent int
    output strings.Builder
}

func NewPrintVisitor() *PrintVisitor {
    return &PrintVisitor{}
}

func (v *PrintVisitor) prefix() string {
    return strings.Repeat("  ", v.indent)
}

func (v *PrintVisitor) VisitNumber(n *NumberLiteral) interface{} {
    return n.String()
}

func (v *PrintVisitor) VisitString(s *StringLiteral) interface{} {
    return s.String()
}

func (v *PrintVisitor) VisitBinary(b *BinaryExpression) interface{} {
    left := b.Left.Accept(v).(string)
    right := b.Right.Accept(v).(string)
    return fmt.Sprintf("(%s %s %s)", left, b.Operator, right)
}

func (v *PrintVisitor) VisitUnary(u *UnaryExpression) interface{} {
    operand := u.Operand.Accept(v).(string)
    return fmt.Sprintf("(%s%s)", u.Operator, operand)
}

func (v *PrintVisitor) VisitFunctionCall(f *FunctionCall) interface{} {
    args := make([]string, len(f.Arguments))
    for i, arg := range f.Arguments {
        args[i] = arg.Accept(v).(string)
    }
    return fmt.Sprintf("%s(%s)", f.FunctionName, strings.Join(args, ", "))
}


// Concrete Visitor 3: TypeCheckVisitor
// Validates that operations are performed on compatible types.
type TypeCheckVisitor struct {
    errors []string
}

func NewTypeCheckVisitor() *TypeCheckVisitor {
    return &TypeCheckVisitor{errors: make([]string, 0)}
}

type ExprType string

const (
    TypeNumber  ExprType = "number"
    TypeString  ExprType = "string"
    TypeBool    ExprType = "bool"
    TypeError   ExprType = "error"
)

func (v *TypeCheckVisitor) VisitNumber(n *NumberLiteral) interface{} {
    return TypeNumber
}

func (v *TypeCheckVisitor) VisitString(s *StringLiteral) interface{} {
    return TypeString
}

func (v *TypeCheckVisitor) VisitBinary(b *BinaryExpression) interface{} {
    leftType := b.Left.Accept(v).(ExprType)
    rightType := b.Right.Accept(v).(ExprType)

    switch b.Operator {
    case "+", "-", "*", "/":
        if leftType != TypeNumber || rightType != TypeNumber {
            if b.Operator == "+" && leftType == TypeString && rightType == TypeString {
                return TypeString // string concatenation OK
            }
            v.errors = append(v.errors, fmt.Sprintf(
                "type error: operator %s requires numbers, got %s and %s",
                b.Operator, leftType, rightType))
            return TypeError
        }
        return TypeNumber
    case "==", "<", ">":
        if leftType != rightType {
            v.errors = append(v.errors, fmt.Sprintf(
                "type error: cannot compare %s with %s", leftType, rightType))
            return TypeError
        }
        return TypeBool
    }
    return TypeError
}

func (v *TypeCheckVisitor) VisitUnary(u *UnaryExpression) interface{} {
    operandType := u.Operand.Accept(v).(ExprType)
    switch u.Operator {
    case "-":
        if operandType != TypeNumber {
            v.errors = append(v.errors, fmt.Sprintf(
                "type error: unary - requires number, got %s", operandType))
            return TypeError
        }
        return TypeNumber
    case "!":
        if operandType != TypeBool {
            v.errors = append(v.errors, fmt.Sprintf(
                "type error: unary ! requires bool, got %s", operandType))
            return TypeError
        }
        return TypeBool
    }
    return TypeError
}

func (v *TypeCheckVisitor) VisitFunctionCall(f *FunctionCall) interface{} {
    // Simplified: assume all functions return a number
    return TypeNumber
}

func (v *TypeCheckVisitor) Errors() []string { return v.errors }
func (v *TypeCheckVisitor) HasErrors() bool  { return len(v.errors) > 0 }


// Concrete Visitor 4: OptimizeVisitor
// Performs constant folding — simplifying expressions that can be evaluated at compile time.
type OptimizeVisitor struct{}

func (v *OptimizeVisitor) VisitNumber(n *NumberLiteral) interface{} { return n }
func (v *OptimizeVisitor) VisitString(s *StringLiteral) interface{} { return s }

func (v *OptimizeVisitor) VisitBinary(b *BinaryExpression) interface{} {
    // Optimize children first
    left := b.Left.Accept(v).(Node)
    right := b.Right.Accept(v).(Node)

    // If both are literals, fold them into one literal
    leftNum, leftIsNum := left.(*NumberLiteral)
    rightNum, rightIsNum := right.(*NumberLiteral)

    if leftIsNum && rightIsNum {
        switch b.Operator {
        case "+":
            return &NumberLiteral{Value: leftNum.Value + rightNum.Value}
        case "-":
            return &NumberLiteral{Value: leftNum.Value - rightNum.Value}
        case "*":
            return &NumberLiteral{Value: leftNum.Value * rightNum.Value}
        case "/":
            if rightNum.Value != 0 {
                return &NumberLiteral{Value: leftNum.Value / rightNum.Value}
            }
        }
    }

    return &BinaryExpression{Left: left, Operator: b.Operator, Right: right}
}

func (v *OptimizeVisitor) VisitUnary(u *UnaryExpression) interface{} {
    operand := u.Operand.Accept(v).(Node)
    if num, ok := operand.(*NumberLiteral); ok && u.Operator == "-" {
        return &NumberLiteral{Value: -num.Value}
    }
    return &UnaryExpression{Operator: u.Operator, Operand: operand}
}

func (v *OptimizeVisitor) VisitFunctionCall(f *FunctionCall) interface{} {
    args := make([]Node, len(f.Arguments))
    for i, arg := range f.Arguments {
        args[i] = arg.Accept(v).(Node)
    }
    return &FunctionCall{FunctionName: f.FunctionName, Arguments: args}
}

Demonstration: Four Operations, One Structure #

func main() {
    // AST for the expression: (2 + 3) * abs(-4)
    expr := &ast.BinaryExpression{
        Left: &ast.BinaryExpression{
            Left:     &ast.NumberLiteral{Value: 2},
            Operator: "+",
            Right:    &ast.NumberLiteral{Value: 3},
        },
        Operator: "*",
        Right: &ast.FunctionCall{
            FunctionName: "abs",
            Arguments: []ast.Node{
                &ast.UnaryExpression{
                    Operator: "-",
                    Operand:  &ast.NumberLiteral{Value: 4},
                },
            },
        },
    }

    // Operation 1: Print the expression
    printer := ast.NewPrintVisitor()
    fmt.Printf("Expression: %v\n", expr.Accept(printer))
    // Output: ((2 + 3) * abs((-4)))

    // Operation 2: Evaluate
    evaluator := ast.NewEvaluateVisitor(nil)
    result := expr.Accept(evaluator)
    fmt.Printf("Result: %v\n", result)
    // Output: 20

    // Operation 3: Type check
    typeChecker := ast.NewTypeCheckVisitor()
    exprType := expr.Accept(typeChecker)
    fmt.Printf("Type: %v\n", exprType)
    if typeChecker.HasErrors() {
        fmt.Printf("Errors: %v\n", typeChecker.Errors())
    }

    // Operation 4: Optimize (constant folding)
    optimizer := &ast.OptimizeVisitor{}
    optimized := expr.Accept(optimizer).(ast.Node)
    fmt.Printf("Optimized: %v\n", optimized.Accept(printer))

    // All the operations above were performed WITHOUT modifying
    // NumberLiteral, BinaryExpression, UnaryExpression, or FunctionCall at all
}
#

Second Case Study: Document Exporter #

A document exporter is a very common Visitor use case in enterprise applications — the document structure stays stable while output formats (PDF, HTML, Markdown) evolve over time.

package document

// DocumentElement is the interface for all document elements.
type DocumentElement interface {
    Accept(visitor DocumentVisitor)
}

// Concrete Elements
type Heading struct {
    Level int
    Text  string
}

func (h *Heading) Accept(v DocumentVisitor) { v.VisitHeading(h) }

type Paragraph struct {
    Text string
}

func (p *Paragraph) Accept(v DocumentVisitor) { v.VisitParagraph(p) }

type Table struct {
    Headers []string
    Rows    [][]string
}

func (t *Table) Accept(v DocumentVisitor) { v.VisitTable(t) }

type CodeBlock struct {
    Language string
    Code     string
}

func (c *CodeBlock) Accept(v DocumentVisitor) { v.VisitCodeBlock(c) }

type Image struct {
    URL    string
    AltText string
    Width  int
}

func (i *Image) Accept(v DocumentVisitor) { v.VisitImage(i) }

// DocumentVisitor defines the operation for every element type.
type DocumentVisitor interface {
    VisitHeading(h *Heading)
    VisitParagraph(p *Paragraph)
    VisitTable(t *Table)
    VisitCodeBlock(c *CodeBlock)
    VisitImage(i *Image)
}

// Document stores all elements and applies a visitor to all of them.
type Document struct {
    Title    string
    Elements []DocumentElement
}

func (d *Document) Accept(visitor DocumentVisitor) {
    for _, elem := range d.Elements {
        elem.Accept(visitor)
    }
}

// MarkdownExporter exports the document to Markdown format.
type MarkdownExporter struct {
    output strings.Builder
}

func NewMarkdownExporter() *MarkdownExporter { return &MarkdownExporter{} }

func (e *MarkdownExporter) VisitHeading(h *Heading) {
    prefix := strings.Repeat("#", h.Level)
    fmt.Fprintf(&e.output, "%s %s\n\n", prefix, h.Text)
}

func (e *MarkdownExporter) VisitParagraph(p *Paragraph) {
    fmt.Fprintf(&e.output, "%s\n\n", p.Text)
}

func (e *MarkdownExporter) VisitTable(t *Table) {
    // Header
    fmt.Fprintf(&e.output, "| %s |\n", strings.Join(t.Headers, " | "))
    divider := make([]string, len(t.Headers))
    for i := range divider {
        divider[i] = "---"
    }
    fmt.Fprintf(&e.output, "| %s |\n", strings.Join(divider, " | "))
    // Rows
    for _, row := range t.Rows {
        fmt.Fprintf(&e.output, "| %s |\n", strings.Join(row, " | "))
    }
    fmt.Fprintln(&e.output)
}

func (e *MarkdownExporter) VisitCodeBlock(c *CodeBlock) {
    fmt.Fprintf(&e.output, "```%s\n%s\n```\n\n", c.Language, c.Code)
}

func (e *MarkdownExporter) VisitImage(i *Image) {
    fmt.Fprintf(&e.output, "![%s](%s)\n\n", i.AltText, i.URL)
}

func (e *MarkdownExporter) Result() string { return e.output.String() }


// HTMLExporter exports the document to HTML format.
type HTMLExporter struct {
    output strings.Builder
}

func NewHTMLExporter() *HTMLExporter {
    e := &HTMLExporter{}
    e.output.WriteString("<!DOCTYPE html>\n<html>\n<body>\n")
    return e
}

func (e *HTMLExporter) VisitHeading(h *Heading) {
    fmt.Fprintf(&e.output, "<h%d>%s</h%d>\n", h.Level, h.Text, h.Level)
}

func (e *HTMLExporter) VisitParagraph(p *Paragraph) {
    fmt.Fprintf(&e.output, "<p>%s</p>\n", p.Text)
}

func (e *HTMLExporter) VisitTable(t *Table) {
    e.output.WriteString("<table>\n<tr>")
    for _, h := range t.Headers {
        fmt.Fprintf(&e.output, "<th>%s</th>", h)
    }
    e.output.WriteString("</tr>\n")
    for _, row := range t.Rows {
        e.output.WriteString("<tr>")
        for _, cell := range row {
            fmt.Fprintf(&e.output, "<td>%s</td>", cell)
        }
        e.output.WriteString("</tr>\n")
    }
    e.output.WriteString("</table>\n")
}

func (e *HTMLExporter) VisitCodeBlock(c *CodeBlock) {
    fmt.Fprintf(&e.output, "<pre><code class=\"language-%s\">%s</code></pre>\n",
        c.Language, c.Code)
}

func (e *HTMLExporter) VisitImage(i *Image) {
    fmt.Fprintf(&e.output, "<img src=%q alt=%q width=%d />\n",
        i.URL, i.AltText, i.Width)
}

func (e *HTMLExporter) Result() string {
    return e.output.String() + "</body>\n</html>"
}


// Usage — the document structure is built once and exported to various formats
func exportDocument() {
    doc := &Document{
        Title: "Golang Tutorial",
        Elements: []DocumentElement{
            &Heading{Level: 1, Text: "Learning Golang"},
            &Paragraph{Text: "Go is a programming language developed by Google."},
            &CodeBlock{Language: "go", Code: `fmt.Println("Hello, World!")`},
            &Table{
                Headers: []string{"Feature", "Go", "Python"},
                Rows: [][]string{
                    {"Typing", "Static", "Dynamic"},
                    {"Performance", "Fast", "Moderate"},
                },
            },
        },
    }

    // Export to Markdown — does not modify the doc at all
    mdExporter := NewMarkdownExporter()
    doc.Accept(mdExporter)
    fmt.Println(mdExporter.Result())

    // Export to HTML — also does not modify the doc
    htmlExporter := NewHTMLExporter()
    doc.Accept(htmlExporter)
    fmt.Println(htmlExporter.Result())

    // Add a PDF exporter tomorrow? Just create a new PDFExporter
    // — no changes to Heading, Paragraph, Table, CodeBlock, Image
}
#

Visitors with Accumulator State #

Some Visitors need to accumulate results during traversal. The accumulation state is stored inside the Visitor itself.

// WordCountVisitor counts the number of words in a document.
type WordCountVisitor struct {
    WordCount int
    CharCount int
    HeadingCount int
}

func (v *WordCountVisitor) VisitHeading(h *Heading) {
    v.HeadingCount++
    words := strings.Fields(h.Text)
    v.WordCount += len(words)
    v.CharCount += len(h.Text)
}

func (v *WordCountVisitor) VisitParagraph(p *Paragraph) {
    words := strings.Fields(p.Text)
    v.WordCount += len(words)
    v.CharCount += len(p.Text)
}

func (v *WordCountVisitor) VisitTable(t *Table) {
    for _, row := range t.Rows {
        for _, cell := range row {
            v.WordCount += len(strings.Fields(cell))
        }
    }
}

func (v *WordCountVisitor) VisitCodeBlock(c *CodeBlock) {
    // Do not count code as normal words
}

func (v *WordCountVisitor) VisitImage(i *Image) {
    // Images have no words
}

// Report returns a summary
func (v *WordCountVisitor) Report() string {
    return fmt.Sprintf("Words: %d, Characters: %d, Headings: %d",
        v.WordCount, v.CharCount, v.HeadingCount)
}
#

Testing the Visitor Pattern #

func TestEvaluateVisitor_BasicArithmetic(t *testing.T) {
    tests := []struct {
        name     string
        expr     Node
        expected float64
    }{
        {
            name: "addition",
            expr: &BinaryExpression{
                Left:     &NumberLiteral{Value: 3},
                Operator: "+",
                Right:    &NumberLiteral{Value: 4},
            },
            expected: 7,
        },
        {
            name: "nested expression",
            expr: &BinaryExpression{
                Left: &BinaryExpression{
                    Left:     &NumberLiteral{Value: 2},
                    Operator: "*",
                    Right:    &NumberLiteral{Value: 3},
                },
                Operator: "+",
                Right:    &NumberLiteral{Value: 1},
            },
            expected: 7,
        },
        {
            name: "unary negation",
            expr: &UnaryExpression{
                Operator: "-",
                Operand:  &NumberLiteral{Value: 5},
            },
            expected: -5,
        },
    }

    evaluator := NewEvaluateVisitor(nil)
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := tt.expr.Accept(evaluator)
            if result.(float64) != tt.expected {
                t.Errorf("expected %v, got %v", tt.expected, result)
            }
        })
    }
}

func TestTypeCheckVisitor_DetectsTypeMismatch(t *testing.T) {
    // String + Number = type error
    expr := &BinaryExpression{
        Left:     &StringLiteral{Value: "hello"},
        Operator: "+",
        Right:    &NumberLiteral{Value: 42},
    }

    checker := NewTypeCheckVisitor()
    expr.Accept(checker)

    if !checker.HasErrors() {
        t.Error("expected type error for string + number")
    }
}

func TestOptimizeVisitor_ConstantFolding(t *testing.T) {
    // (2 + 3) * 4 should fold into 20
    expr := &BinaryExpression{
        Left: &BinaryExpression{
            Left:     &NumberLiteral{Value: 2},
            Operator: "+",
            Right:    &NumberLiteral{Value: 3},
        },
        Operator: "*",
        Right:    &NumberLiteral{Value: 4},
    }

    optimizer := &OptimizeVisitor{}
    optimized := expr.Accept(optimizer).(Node)

    // After optimization, it must be a NumberLiteral{20}
    num, ok := optimized.(*NumberLiteral)
    if !ok {
        t.Fatalf("expected NumberLiteral after optimization, got %T", optimized)
    }
    if num.Value != 20 {
        t.Errorf("expected 20, got %v", num.Value)
    }
}

func TestMarkdownExporter_HeadingFormat(t *testing.T) {
    doc := &Document{
        Elements: []DocumentElement{
            &Heading{Level: 1, Text: "Title"},
            &Heading{Level: 2, Text: "Subtitle"},
        },
    }

    exporter := NewMarkdownExporter()
    doc.Accept(exporter)
    result := exporter.Result()

    if !strings.Contains(result, "# Title") {
        t.Error("expected H1 to be formatted as # Title")
    }
    if !strings.Contains(result, "## Subtitle") {
        t.Error("expected H2 to be formatted as ## Subtitle")
    }
}

func TestWordCountVisitor_AccumulatesCorrectly(t *testing.T) {
    doc := &Document{
        Elements: []DocumentElement{
            &Heading{Level: 1, Text: "Hello World"},
            &Paragraph{Text: "This is a test paragraph"},
            &CodeBlock{Language: "go", Code: "fmt.Println()"},
        },
    }

    counter := &WordCountVisitor{}
    doc.Accept(counter)

    if counter.WordCount != 7 { // "Hello World" (2) + "This is a test paragraph" (5)
        t.Errorf("expected 7 words, got %d", counter.WordCount)
    }
    if counter.HeadingCount != 1 {
        t.Errorf("expected 1 heading, got %d", counter.HeadingCount)
    }
}
#

When to Use and When Not to #

USE Visitor if:
  ✓ You need to add many different operations to a stable object structure
  ✓ The object structure rarely changes but operations grow often
  ✓ The same operation needs to be performed on every element in a hierarchy
  ✓ You want to separate algorithms from data structures (AST, documents, expression trees)
  ✓ You need to accumulate state during traversal

AVOID Visitor if:
  ✗ The object structure changes often — every added node requires updating ALL visitors
  ✗ There are only 1-2 operations — plain methods on the class are simpler
  ✗ The operations are very simple — Visitor adds unnecessary complexity
  ✗ The components in the structure are very heterogeneous — the Visitor interface gets too large

Visitor Is Not for Structures That Change Often

The Visitor Pattern’s trade-off is the opposite of most other patterns: adding a new operation is easy (one new Visitor), but adding a new node type is hard (every existing Visitor must be updated). If you add new node types more often than new operations, Visitor is not the right choice.


Visitor Review Checklist #

DESIGN:
  □ Every concrete element implements Accept(visitor Visitor)
  □ Accept only calls visitor.VisitXxx(self) — no other logic
  □ The Visitor interface has a method for every element type
  □ Operations are centralized in Visitors, not scattered across elements

DOUBLE DISPATCH:
  □ Accept is called on the element → the element calls the right Visit method
  □ The client does no type assertions to choose the Visit method

ACCUMULATOR STATE:
  □ Visitor state (output, counters) is initialized correctly
  □ Visitors can be reused after Reset() if needed

TESTING:
  □ Each Visitor is tested in isolation with mock nodes
  □ Recursive traversal is verified (nested expressions, nested documents)
  □ Visitors with state have their accumulation verified
  □ Optimization visitors have their results verified (constant folding)

Summary #

  • Visitor separates operations from structure — elements do not change when new operations are added; just create a new Visitor.
  • Double dispatch is the key mechanismelement.Accept(visitor) followed by visitor.VisitElement(element) ensures the correct implementation is called based on the concrete types of both.
  • The opposite trade-off: easy to add operations (new Visitor), but hard to add new element types (every Visitor must be updated).
  • Four Visitors for an AST: EvaluateVisitor, PrintVisitor, TypeCheckVisitor, OptimizeVisitor — none of them change a single line in NumberLiteral, BinaryExpression, etc.
  • Accumulator state lives in the Visitor: word counts, HTML/Markdown output, error lists — all stored inside the Visitor, not in the elements.
  • Visitor only fits stable structures — if element types grow often, consider another approach like direct methods or Strategy.
  • Document exporters are a classic use case besides AST — Heading, Paragraph, Table, CodeBlock stay stable; MarkdownExporter, HTMLExporter, PDFExporter can be added freely.
  • Testing Visitors is easy: each Visitor is tested in isolation; mock elements can be minimal, only implementing Accept.

← Previous: Iterator   Next: Interpreter →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact