An object hierarchy needs new operations added regularly, and each new operation touches every node class. The node hierarchy is stable (file, directory, symlink) but the operation set is open-ended (size, count, search, audit, export). Every new operation is Shotgun Surgery on the node hierarchy.
Operations scattered across every node class as methods the agent must verify uniformly on every operation-shape change. Adding 'searchByName' touches every node type; missing one is a type-compatible silent bug.
Each operation is a Visitor class with one visit method per node type. Node classes implement accept(visitor) and delegate dispatch via double dispatch (node.accept calls visitor.visitX(node)). New operations are new Visitor classes; node classes never change.
Per-operation visitors the agent reads as one file; per-node node classes the agent reads as small data + accept-dispatch. Static analysis enumerates operations by listing Visitor subclasses; static analysis enumerates node types by listing nodes; the two axes are independent.
Before the pattern
class FileNode {constructor(name, bytes) {this.name = name;this.bytes = bytes;}totalSize() { return this.bytes; }countFiles() { return 1; }findLargest() { return this; }}class DirectoryNode {constructor(name, children) {this.name = name;this.children = children;}totalSize() {return this.children.reduce((s, c) => s + c.totalSize(), 0);}countFiles() {return this.children.reduce((s, c) => s + c.countFiles(), 0);}findLargest() {let largest = null;for (const child of this.children) {const x = child.findLargest();if (!largest || x.bytes > largest.bytes) largest = x;}return largest;}}// Adding 'searchByName' = edits in every node class. Each new operation// touches the node hierarchy; node classes change for unrelated reasons.
After the pattern
class FileNode {constructor(name, bytes) {this.name = name;this.bytes = bytes;}accept(visitor) { return visitor.visitFile(this); }}class DirectoryNode {constructor(name, children) {this.name = name;this.children = children;}accept(visitor) { return visitor.visitDirectory(this); }}class TotalSizeVisitor {visitFile(file) { return file.bytes; }visitDirectory(dir) {return dir.children.reduce((s, c) => s + c.accept(this), 0);}}class CountFilesVisitor {visitFile() { return 1; }visitDirectory(dir) {return dir.children.reduce((s, c) => s + c.accept(this), 0);}}class FindLargestVisitor {visitFile(file) { return file; }visitDirectory(dir) {let largest = null;for (const child of dir.children) {const candidate = child.accept(this);if (!largest || candidate.bytes > largest.bytes) largest = candidate;}return largest;}}const size = root.accept(new TotalSizeVisitor());const count = root.accept(new CountFilesVisitor());
Operations as node-class methods make every new operation Divergent Change on the nodes (which change for reasons unrelated to their data shape) AND Shotgun Surgery (edits ripple through every node type). The node classes become god classes accumulating every operation the system has ever wanted to perform on them.
Operations-as-methods is N nodes × M operations cells the agent verifies on every operation addition. Adding M+1 forces editing N nodes; one missed node ships as a runtime error the test suite may not catch on the less-exercised variant.
The Visitor pattern inverts the dependency: adding a new node type now becomes Shotgun Surgery on every existing visitor — the tradeoff that prevents Visitor from being a universal answer. Choose Visitor when operations vary often and node types rarely; flip back when the opposite holds.
Visitor inversion means adding a node is N visitors × 1 method cells the agent edits. The pattern only wins when M (operations) grows faster than N (node types); when both grow, the pattern reorders the problem rather than solving it.
Per-operation reasoning is one-visitor-file; per-node reasoning is one-node-file (small, focused on data + accept). Tests for each visitor cover one operation against every node type; tests for the node hierarchy cover its accept dispatch generically.
Per-operation edits scope to one visitor file; per-node-traversal verification is structural in accept dispatch; visitor tests cover an entire operation against every node type compositionally without enumerating combinations by hand.
Visitors that need state across the traversal (running totals, accumulated paths, traversal-time stack) reintroduce coupling between visit methods. Without careful state management, the visitor becomes a stateful god object the agent or human must reason about across visit calls — exactly what the pattern's per-visit-method shape was supposed to prevent.
Visitors with hidden state across visit calls produce traversal-order-dependent bugs the agent cannot localize from any one visit method. Stack traces span accept + visit boundaries; the agent must reason about the interleaving of accept and visit calls on every traversal-related bug.