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…