Visitor Pattern #

Bayangkan sebuah Abstract Syntax Tree (AST) yang merepresentasikan kode program — terdiri dari node-node seperti NumberLiteral, BinaryExpression, FunctionCall, dan VariableReference. Kamu perlu melakukan beberapa operasi berbeda pada AST yang sama: evaluasi untuk menjalankan kode, pretty-printing untuk menampilkan kode, type-checking untuk validasi, dan optimisasi untuk menyederhanakan ekspresi. Tanpa Visitor Pattern, setiap operasi baru harus ditambahkan sebagai method baru ke semua node — mengubah NumberLiteral, BinaryExpression, FunctionCall, dan seterusnya. Dengan Visitor Pattern, setiap operasi menjadi satu class Visitor yang mandiri; node-node tidak perlu diubah sama sekali. Menambah operasi baru = menambah satu Visitor baru.

Apa itu Visitor Pattern? #

Visitor Pattern adalah behavioral design pattern yang memungkinkan kamu menambahkan operasi baru ke struktur objek yang sudah ada tanpa mengubah class-class dalam struktur tersebut. Visitor memisahkan algoritma dari struktur objek yang ia operasikan — struktur objek tetap stabil, operasi bisa ditambahkan secara bebas.

Di Golang yang tidak punya method overloading, Visitor diimplementasikan melalui double dispatch — dua pemanggilan method yang bekerja bersama untuk memastikan implementasi yang tepat dipanggil berdasarkan tipe konkret dua objek secara bersamaan.

Dua properti kunci Visitor Pattern:

  • Open for extension, closed for modification — menambah operasi baru tidak mengubah struktur objek yang sudah ada
  • Operasi terpusat — semua logika untuk satu operasi ada di satu Visitor, bukan tersebar di banyak class
flowchart LR
    subgraph "Tanpa Visitor — Operasi Tersebar"
        NL[NumberLiteral\n+ Evaluate()\n+ Print()\n+ TypeCheck()\n+ Optimize()]
        BE[BinaryExpr\n+ Evaluate()\n+ Print()\n+ TypeCheck()\n+ Optimize()]
        note1["Tambah operasi baru\n= ubah SEMUA node"]
    end

    subgraph "Dengan Visitor — Operasi Terpusat"
        N2[NumberLiteral\n+ Accept(visitor)]
        B2[BinaryExpr\n+ Accept(visitor)]
        EV[EvaluateVisitor\n+ VisitNumber()\n+ VisitBinary()]
        PR[PrintVisitor\n+ VisitNumber()\n+ VisitBinary()]
        note2["Tambah operasi baru\n= tambah 1 Visitor baru\nNode tidak diubah"]
        N2 & B2 -->|Accept| EV & PR
    end

Double Dispatch: Mekanisme Inti Visitor #

Di bahasa dengan method overloading (Java, C++), dispatch berdasarkan tipe runtime bisa terjadi secara otomatis. Golang tidak punya ini, sehingga kita menggunakan double dispatch secara eksplisit.

// Tanpa double dispatch — tipe konkret hilang setelah masuk ke interface
func processNode(node Node, visitor Visitor) {
    // visitor.Visit(node) — tidak bisa tahu node adalah NumberLiteral atau BinaryExpr!
}

// Double dispatch — dua "dispatch" yang bekerja bersama:
// Dispatch 1: node.Accept(visitor) — berdasarkan tipe konkret node
// Dispatch 2: visitor.VisitNumber(n) — berdasarkan tipe konkret visitor

type Node interface {
    Accept(visitor Visitor) // setiap node tahu cara "menerima" visitor
}

type NumberLiteral struct{ Value float64 }

func (n *NumberLiteral) Accept(visitor Visitor) {
    visitor.VisitNumber(n) // dispatch ke method yang tepat berdasarkan tipe node
}

type BinaryExpr struct {
    Left, Right Node
    Operator    string
}

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

// Visitor tahu method mana yang dipanggil berdasarkan tipe node
type Visitor interface {
    VisitNumber(n *NumberLiteral)
    VisitBinary(b *BinaryExpr)
}

Alur double dispatch:

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

    C->>N: Accept(evaluateVisitor)
    Note over N: Dispatch 1: node tahu tipe dirinya sendiri
    N->>V: VisitNumber(self)
    Note over V: Dispatch 2: visitor tahu cara handle NumberLiteral
    V-->>C: result

Implementasi Lengkap: AST Expression Processor #

Element Hierarchy (Node-node yang Tidak Berubah) #

package ast

import "fmt"

// Node adalah interface yang diimplementasikan semua node AST.
type Node interface {
    Accept(visitor Visitor) interface{}
    String() string
}

// NumberLiteral merepresentasikan bilangan konstan.
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 merepresentasikan string konstan.
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 merepresentasikan operasi biner (a + b, a * b, dll).
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 merepresentasikan operasi unary (-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 merepresentasikan pemanggilan fungsi.
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 mendefinisikan operasi untuk setiap tipe node.
// Setiap Visitor baru yang diimplementasikan menambah operasi baru
// tanpa mengubah satu pun node di atas.
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 mengevaluasi ekspresi dan mengembalikan nilai numerik atau string.
// Ini adalah operasi baru yang ditambahkan TANPA mengubah node apapun.
type EvaluateVisitor struct {
    variables map[string]interface{} // untuk resolusi variabel
}

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)

    // Aritmatika numerik
    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
        }
    }

    // Konkatenasi string
    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 menghasilkan representasi string yang bisa dibaca dari AST.
// Operasi baru ini ditambahkan tanpa mengubah satu pun 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
// Memvalidasi bahwa operasi dilakukan pada tipe yang kompatibel.
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: anggap semua fungsi return 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
// Melakukan constant folding — menyederhanakan ekspresi yang bisa dievaluasi 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{} {
    // Optimisasi child dulu
    left := b.Left.Accept(v).(Node)
    right := b.Right.Accept(v).(Node)

    // Jika keduanya literal, fold menjadi satu 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}
}

Demonstrasi: Empat Operasi, Satu Struktur #

func main() {
    // AST untuk ekspresi: (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},
                },
            },
        },
    }

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

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

    // Operasi 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())
    }

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

    // Semua operasi di atas dilakukan TANPA mengubah
    // NumberLiteral, BinaryExpression, UnaryExpression, atau FunctionCall sama sekali
}

Studi Kasus Kedua: Document Exporter #

Document exporter adalah use case Visitor yang sangat umum di aplikasi enterprise — struktur dokumen tetap stabil, format output (PDF, HTML, Markdown) berkembang seiring waktu.

package document

// DocumentElement adalah interface untuk semua elemen dokumen.
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 mendefinisikan operasi untuk setiap tipe elemen.
type DocumentVisitor interface {
    VisitHeading(h *Heading)
    VisitParagraph(p *Paragraph)
    VisitTable(t *Table)
    VisitCodeBlock(c *CodeBlock)
    VisitImage(i *Image)
}

// Document menyimpan semua elemen dan menerapkan visitor ke semuanya.
type Document struct {
    Title    string
    Elements []DocumentElement
}

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

// MarkdownExporter mengekspor dokumen ke format Markdown.
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 mengekspor dokumen ke format HTML.
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>"
}


// Penggunaan — struktur dokumen dibuat sekali, diekspor ke berbagai format
func exportDocument() {
    doc := &Document{
        Title: "Tutorial Golang",
        Elements: []DocumentElement{
            &Heading{Level: 1, Text: "Belajar Golang"},
            &Paragraph{Text: "Golang adalah bahasa pemrograman yang dikembangkan oleh Google."},
            &CodeBlock{Language: "go", Code: `fmt.Println("Hello, World!")`},
            &Table{
                Headers: []string{"Feature", "Go", "Python"},
                Rows: [][]string{
                    {"Typing", "Static", "Dynamic"},
                    {"Performance", "Fast", "Moderate"},
                },
            },
        },
    }

    // Ekspor ke Markdown — tidak mengubah doc sama sekali
    mdExporter := NewMarkdownExporter()
    doc.Accept(mdExporter)
    fmt.Println(mdExporter.Result())

    // Ekspor ke HTML — juga tidak mengubah doc
    htmlExporter := NewHTMLExporter()
    doc.Accept(htmlExporter)
    fmt.Println(htmlExporter.Result())

    // Tambah PDF exporter besok? Cukup buat PDFExporter baru
    // — tidak ada perubahan pada Heading, Paragraph, Table, CodeBlock, Image
}

Visitor dengan Accumulator State #

Beberapa Visitor perlu mengakumulasi hasil selama traversal. State akumulasi disimpan di dalam Visitor itu sendiri.

// WordCountVisitor menghitung jumlah kata dalam dokumen.
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) {
    // Jangan hitung kode sebagai kata-kata normal
}

func (v *WordCountVisitor) VisitImage(i *Image) {
    // Gambar tidak punya kata
}

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

Testing 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 harus di-fold menjadi 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)

    // Setelah optimisasi, harus menjadi 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)
    }
}

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Visitor jika:
  ✓ Perlu menambah banyak operasi berbeda ke struktur objek yang stabil
  ✓ Struktur objek jarang berubah tapi operasi sering bertambah
  ✓ Operasi yang sama perlu dilakukan ke semua elemen dalam hierarki
  ✓ Ingin memisahkan algoritma dari struktur data (AST, dokumen, expression tree)
  ✓ Perlu mengakumulasi state selama traversal

HINDARI Visitor jika:
  ✗ Struktur objek sering berubah — setiap penambahan node memerlukan update SEMUA visitor
  ✗ Hanya ada 1-2 operasi — method biasa di class lebih sederhana
  ✗ Operasi sangat sederhana — Visitor menambah kompleksitas yang tidak perlu
  ✗ Komponen dalam struktur sangat heterogen — Visitor interface akan terlalu besar

Visitor Bukan untuk Struktur yang Sering Berubah

Trade-off Visitor Pattern kebalikan dari kebanyakan pattern lain: menambah operasi baru mudah (satu Visitor baru), tapi menambah tipe node baru sulit (semua Visitor yang ada harus diupdate). Jika kamu lebih sering menambah tipe node baru daripada operasi baru, Visitor bukan pilihan yang tepat.


Checklist Review Visitor #

DESAIN:
  □ Setiap concrete element mengimplementasikan Accept(visitor Visitor)
  □ Accept hanya memanggil visitor.VisitXxx(self) — tidak ada logika lain
  □ Visitor interface punya method untuk setiap tipe element
  □ Operasi terpusat di Visitor, bukan tersebar di element

DOUBLE DISPATCH:
  □ Accept dipanggil pada element → element memanggil Visit method yang tepat
  □ Client tidak melakukan type assertion untuk memilih Visit method

STATE AKUMULASI:
  □ State visitor (output, counter) diinisialisasi dengan benar
  □ Visitor bisa digunakan kembali setelah Reset() jika perlu

TESTING:
  □ Setiap Visitor di-test secara terisolasi dengan node mock
  □ Traversal rekursif diverifikasi (nested expression, nested document)
  □ Visitor dengan state diverifikasi akumulasinya
  □ Optimisasi visitor diverifikasi hasilnya (constant folding)

Ringkasan #

  • Visitor memisahkan operasi dari struktur — element tidak berubah saat operasi baru ditambahkan; cukup buat Visitor baru.
  • Double dispatch adalah mekanisme kuncielement.Accept(visitor) kemudian visitor.VisitElement(element) memastikan implementasi yang tepat dipanggil berdasarkan tipe konkret keduanya.
  • Trade-off yang berlawanan: mudah menambah operasi (Visitor baru), tapi sulit menambah tipe element baru (semua Visitor harus diupdate).
  • Empat Visitor untuk AST: EvaluateVisitor, PrintVisitor, TypeCheckVisitor, OptimizeVisitor — semuanya tidak mengubah satu baris kode di NumberLiteral, BinaryExpression, dll.
  • State akumulasi di Visitor: WordCount, output HTML/Markdown, error list — semua disimpan di dalam Visitor, bukan di element.
  • Visitor hanya cocok untuk struktur yang stabil — jika tipe element sering bertambah, pertimbangkan pendekatan lain seperti method langsung atau Strategy.
  • Document exporter adalah use case klasik selain AST — Heading, Paragraph, Table, CodeBlock tetap stabil; MarkdownExporter, HTMLExporter, PDFExporter bisa ditambah bebas.
  • Testing Visitor mudah: setiap Visitor di-test secara terisolasi; mock element bisa dibuat minimal hanya dengan mengimplementasikan Accept.

← Sebelumnya: Iterator   Berikutnya: Interpreter →

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