From harness-claude
Implements the Visitor pattern for adding operations to object structures via double dispatch. Includes TypeScript examples for AST evaluation, printing, and counting.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:gof-visitor-patternThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Add operations to object structures without modifying them using double dispatch.
Add operations to object structures without modifying them using double dispatch.
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 };
}
}
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:
accept() — breaks double dispatchDouble 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.
refactoring.guru/design-patterns/visitor
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeAdds new operations to object structures without modifying them, using the Visitor pattern with JavaScript examples for AST, file trees, and DOM.
Adds new operations to stable object structures without modifying element classes, using the Visitor pattern. Useful for AST analysis, code quality tools, and compiler passes.
Generates Visitor pattern for PHP 8.4 to perform operations on object structures without modifying element classes. Includes visitor interface, concrete visitors, visitable elements, and unit tests.