iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Visitor

The Visitor pattern separates an operation from the structure it operates on. Define a visitor interface with one method per concrete element type; new operations become new visitors without touching the element classes. The trade-off: hard to add new element types (every visitor has to change), easy to add new operations.

A typed AST visitor with multiple passes

EXAMPLE
<?php
// ============================================================
// Element hierarchy — the structure we visit
// ============================================================
interface Node {
    public function accept(Visitor $v);
}

class Number implements Node {
    public function __construct(public float $value) {}
    public function accept(Visitor $v) { return $v->visitNumber($this); }
}

class Variable implements Node {
    public function __construct(public string $name) {}
    public function accept(Visitor $v) { return $v->visitVariable($this); }
}

class BinaryOp implements Node {
    public function __construct(public string $op, public Node $left, public Node $right) {}
    public function accept(Visitor $v) { return $v->visitBinaryOp($this); }
}

class FunctionCall implements Node {
    /** @param Node[] $args */
    public function __construct(public string $name, public array $args) {}
    public function accept(Visitor $v) { return $v->visitFunctionCall($this); }
}

// ============================================================
// Visitor interface — one method per concrete element type
// ============================================================
interface Visitor {
    public function visitNumber(Number $n);
    public function visitVariable(Variable $v);
    public function visitBinaryOp(BinaryOp $op);
    public function visitFunctionCall(FunctionCall $f);
}

// ============================================================
// Concrete visitor #1: pretty-printer
// ============================================================
class Printer implements Visitor {
    public function visitNumber(Number $n)         { return (string) $n->value; }
    public function visitVariable(Variable $v)     { return $v->name; }
    public function visitBinaryOp(BinaryOp $op)    {
        return '(' . $op->left->accept($this) . ' ' . $op->op . ' ' . $op->right->accept($this) . ')';
    }
    public function visitFunctionCall(FunctionCall $f) {
        $args = array_map(fn($a) => $a->accept($this), $f->args);
        return $f->name . '(' . implode(', ', $args) . ')';
    }
}

// ============================================================
// Concrete visitor #2: evaluator (with an environment)
// ============================================================
class Evaluator implements Visitor {
    public function __construct(private array $env = []) {}
    public function visitNumber(Number $n)      { return $n->value; }
    public function visitVariable(Variable $v)  {
        if (!array_key_exists($v->name, $this->env)) throw new RuntimeException('unknown: ' . $v->name);
        return $this->env[$v->name];
    }
    public function visitBinaryOp(BinaryOp $op) {
        $l = $op->left->accept($this);
        $r = $op->right->accept($this);
        return match ($op->op) { '+' => $l + $r, '-' => $l - $r, '*' => $l * $r, '/' => $l / $r };
    }
    public function visitFunctionCall(FunctionCall $f) {
        $args = array_map(fn($a) => $a->accept($this), $f->args);
        return match ($f->name) {
            'sqrt' => sqrt($args[0]),
            'max'  => max($args),
            default => throw new RuntimeException('unknown fn: ' . $f->name),
        };
    }
}

// ============================================================
// Concrete visitor #3: collect all free variable names (a 'pass')
// ============================================================
class CollectVars implements Visitor {
    public array $names = [];
    public function visitNumber(Number $n)         {}
    public function visitVariable(Variable $v)     { $this->names[$v->name] = true; }
    public function visitBinaryOp(BinaryOp $op)    { $op->left->accept($this); $op->right->accept($this); }
    public function visitFunctionCall(FunctionCall $f) {
        foreach ($f->args as $a) $a->accept($this);
    }
}

// ============================================================
// Demo — same AST, three operations
// ============================================================
$ast = new BinaryOp('+',
    new FunctionCall('sqrt', [new Variable('x')]),
    new BinaryOp('*', new Number(2.5), new Variable('y')));

echo $ast->accept(new Printer()), "\n";
echo $ast->accept(new Evaluator(['x' => 16, 'y' => 4])), "\n";

$cv = new CollectVars();
$ast->accept($cv);
print_r(array_keys($cv->names));

Why it matters

Visitor is exactly the right call when your operations grow faster than your element types. Compilers, ASTs, document trees, file-format parsers all qualify. If new ELEMENT types arrive often (e.g. a UI component library), Visitor becomes painful — every new component forces a change to every visitor. Pick the pattern based on which axis varies.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Add operations to a class hierarchy without modifying the classes.
// Each node accepts a visitor and dispatches by type.
class Circle  { accept(v) { return v.visitCircle(this); } }
class Square  { accept(v) { return v.visitSquare(this); } }
const areaVisitor = {
    visitCircle: c => Math.PI * c.r * c.r,
    visitSquare: s => s.side * s.side,
};
Try it Yourself »

Discussion

Loading…