A recursive structure has accumulated multiple operations (compute, print, depth, summarize), each spread across every node class. Adding a new operation means editing every class; each class changes for many unrelated reasons; per-operation code scatters across the type hierarchy.
Operations live in Visitor classes; the structure exposes only `accept(visitor)`. Adding an operation is one new Visitor; the structure stays closed to operation changes (open/closed at the dispatch level).
Before the refactoring
// Each operation (compute, print, depth) is a method on every node;// adding an operation means editing every class.class NumberExpr {constructor(value) { this.value = value; }compute() { return this.value; }print() { return String(this.value); }depth() { return 1; }}class AddExpr {constructor(left, right) { this.left = left; this.right = right; }compute() { return this.left.compute() + this.right.compute(); }print() { return '(' + this.left.print() + ' + ' + this.right.print() + ')'; }depth() { return 1 + Math.max(this.left.depth(), this.right.depth()); }}class MultiplyExpr {constructor(left, right) { this.left = left; this.right = right; }compute() { return this.left.compute() * this.right.compute(); }print() { return this.left.print() + ' * ' + this.right.print(); }depth() { return 1 + Math.max(this.left.depth(), this.right.depth()); }}
After the refactoring
// Nodes accept visitors; each operation is a visitor class.// New operations land as new visitors; the node classes stay closed.class NumberExpr {constructor(value) { this.value = value; }accept(visitor) { return visitor.visitNumber(this); }}class AddExpr {constructor(left, right) { this.left = left; this.right = right; }accept(visitor) { return visitor.visitAdd(this); }}class MultiplyExpr {constructor(left, right) { this.left = left; this.right = right; }accept(visitor) { return visitor.visitMultiply(this); }}class ComputeVisitor {visitNumber(n) { return n.value; }visitAdd(a) { return a.left.accept(this) + a.right.accept(this); }visitMultiply(m) { return m.left.accept(this) * m.right.accept(this); }}class PrintVisitor {visitNumber(n) { return String(n.value); }visitAdd(a) { return '(' + a.left.accept(this) + ' + ' + a.right.accept(this) + ')'; }visitMultiply(m) { return m.left.accept(this) + ' * ' + m.right.accept(this); }}
Each new operation is Shotgun Surgery across the node hierarchy. Operations that share code across nodes (e.g., the recursive descent) duplicate per node, with subtle drift between copies. Tests for operations get tangled with tests for the structure itself.
Visitor inverts the open/closed direction: it makes adding operations cheap but adding new node types expensive (every existing visitor needs a new `visitX` method). The double-dispatch ceremony obscures simple traversals; languages without pattern matching pay extra syntactic cost.
Each operation reads as one cohesive class; the structure's per-node code is just a one-line accept. Diff for a new operation is one new visitor file; tests for the visitor are isolated from the structure's own tests.
Applying Visitor to a structure whose node taxonomy is still volatile produces churn: every node-set change touches every visitor. The pattern earns its keep when the node set is stable and the operation set is growing — not the other way around.