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.
An operation's logic the agent must trace across N node classes to reconstruct what happens on a recursive call. The structure's source files are large because each one carries every operation; adding an operation requires the agent to coordinate edits across the full 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).
One file per operation; the agent verifies a Visitor against its declared interface in isolation. Node classes hold data plus one accept method, dropping the per-operation surface area the agent loads when reading any node type.
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.
Each operation × N node classes = N×M cells the agent must verify match for the operation to behave consistently across the structure. Cross-node invariants (every node implements compute, every node's compute is consistent with its print) are not statically enforceable.
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.
Visitor splits a single conceptual operation across two layers (accept + visit); the agent must follow the double dispatch to trace what runs for a given node + operation pair. Adding a node type requires the agent to edit every visitor — Shotgun Surgery shifts from operations to nodes.
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.
Each operation lives at one Visitor file the agent reads end-to-end; the type checker confirms each Visitor implements every visit method, and adding an operation does not edit any node class.
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.
A visitor hierarchy applied to an unstable node set forces the agent to chase a Shotgun Surgery across visitor files every time a node is added. Per-edit context cost goes up linearly with operation count when nodes change — the inverse of the pattern's intended cost shape.