git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/gof-visitor-pattern" ~/.claude/skills/intense-visions-harness-engineering-gof-visitor-pattern-39edb0 && rm -rf "$T"
agents/skills/codex/gof-visitor-pattern/SKILL.mdGOF Visitor Pattern
Add operations to object structures without modifying them using double dispatch.
When to Use
- You need to add new operations to a stable object hierarchy without modifying those classes
- You're processing ASTs, expression trees, file system hierarchies, or DOM trees
- You have many unrelated operations on the same object structure and don't want to pollute it
- You want to accumulate state across a traversal (collecting, counting, validating)
Instructions
Double dispatch visitor:
// Element interface — accept any visitor interface Expression { accept<T>(visitor: ExpressionVisitor<T>): T; } // Concrete elements class NumberLiteral implements Expression { constructor(public readonly value: number) {} accept<T>(visitor: ExpressionVisitor<T>): T { return visitor.visitNumber(this); } } class AddExpression implements Expression { constructor( public readonly left: Expression, public readonly right: Expression ) {} accept<T>(visitor: ExpressionVisitor<T>): T { return visitor.visitAdd(this); } } class MultiplyExpression implements Expression { constructor( public readonly left: Expression, public readonly right: Expression ) {} accept<T>(visitor: ExpressionVisitor<T>): T { return visitor.visitMultiply(this); } } // Visitor interface — one method per element type interface ExpressionVisitor<T> { visitNumber(node: NumberLiteral): T; visitAdd(node: AddExpression): T; visitMultiply(node: MultiplyExpression): T; } // Concrete visitors — separate operations from the object structure // Evaluate the expression class EvaluateVisitor implements ExpressionVisitor<number> { visitNumber(node: NumberLiteral): number { return node.value; } visitAdd(node: AddExpression): number { return node.left.accept(this) + node.right.accept(this); } visitMultiply(node: MultiplyExpression): number { return node.left.accept(this) * node.right.accept(this); } } // Pretty print the expression class PrintVisitor implements ExpressionVisitor<string> { visitNumber(node: NumberLiteral): string { return `${node.value}`; } visitAdd(node: AddExpression): string { return `(${node.left.accept(this)} + ${node.right.accept(this)})`; } visitMultiply(node: MultiplyExpression): string { return `(${node.left.accept(this)} * ${node.right.accept(this)})`; } } // Count nodes class CountVisitor implements ExpressionVisitor<number> { visitNumber(_: NumberLiteral): number { return 1; } visitAdd(node: AddExpression): number { return 1 + node.left.accept(this) + node.right.accept(this); } visitMultiply(node: MultiplyExpression): number { return 1 + node.left.accept(this) + node.right.accept(this); } } // Build and visit the AST: (2 + 3) * 4 const ast = new MultiplyExpression( new AddExpression(new NumberLiteral(2), new NumberLiteral(3)), new NumberLiteral(4) ); const evaluator = new EvaluateVisitor(); const printer = new PrintVisitor(); const counter = new CountVisitor(); console.log(ast.accept(evaluator)); // 20 console.log(ast.accept(printer)); // ((2 + 3) * 4) console.log(ast.accept(counter)); // 5
Discriminated union alternative (TypeScript idiomatic — often cleaner):
type Expr = | { kind: 'number'; value: number } | { kind: 'add'; left: Expr; right: Expr } | { kind: 'multiply'; left: Expr; right: Expr }; function evaluate(expr: Expr): number { switch (expr.kind) { case 'number': return expr.value; case 'add': return evaluate(expr.left) + evaluate(expr.right); case 'multiply': return evaluate(expr.left) * evaluate(expr.right); // TypeScript errors if a case is missing — exhaustiveness checking } } function print(expr: Expr): string { switch (expr.kind) { case 'number': return `${expr.value}`; case 'add': return `(${print(expr.left)} + ${print(expr.right)})`; case 'multiply': return `(${print(expr.left)} * ${print(expr.right)})`; } }
Stateful visitor (accumulating results):
class FileSystemVisitor { private totalSize = 0; private fileCount = 0; private maxDepth = 0; visitFile(file: File, depth: number): void { this.totalSize += file.size; this.fileCount++; this.maxDepth = Math.max(this.maxDepth, depth); } visitDirectory(dir: Directory, depth: number): void { // No accumulation needed — just recurse for (const child of dir.children) { if (child instanceof File) this.visitFile(child, depth + 1); else if (child instanceof Directory) this.visitDirectory(child, depth + 1); } } getReport(): { totalSize: number; fileCount: number; maxDepth: number } { return { totalSize: this.totalSize, fileCount: this.fileCount, maxDepth: this.maxDepth }; } }
Details
When discriminated unions beat Visitor: If you own both the element types and the operations (no third-party hierarchy to extend), prefer discriminated unions — they're simpler and TypeScript's exhaustiveness checking prevents missing cases. Use Visitor when you need to add operations to a class hierarchy you don't own.
Adding a new element type forces all visitors to update — this is the tradeoff. Adding new operations (new Visitor) is free. Adding new types breaks all existing Visitors. This is the "expression problem."
Anti-patterns:
- Visitor that modifies the element structure — visitors should read/compute, not mutate the structure they traverse
- Visitor methods that call back into visitor methods manually instead of using
— breaks double dispatchaccept() - Using Visitor where a simple recursive function would suffice — prefer the simpler approach
Double dispatch explained: When you call
node.accept(visitor), the element type is resolved (first dispatch). The accept method then calls visitor.visitNumber(this) — the visitor type is resolved (second dispatch). This gives you the right behavior for each element-visitor combination without instanceof checks.
Source
refactoring.guru/design-patterns/visitor
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.