Loading learning content...
Theory is important, but seeing the Visitor pattern applied to real problems cements understanding. In this page, we'll explore comprehensive examples across different domains—from compilers to document processing to file systems.
Each example is complete and demonstrates the pattern's power in production-like scenarios.
By the end of this page, you will see the Visitor pattern solving real problems in multiple domains. You'll understand how to model complex structures, implement multiple visitors, handle composite elements, and deal with practical concerns like error handling and state management.
Let's build a small expression language compiler that demonstrates multiple visitor passes: evaluation, type checking, optimization, and code generation. This is the canonical Visitor use case.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
// ═══════════════════════════════════════════════════════════════// EXPRESSION LANGUAGE: A Complete Multi-Visitor Example// ═══════════════════════════════════════════════════════════════ // ─── Type System ─────────────────────────────────────────────── type ExprType = 'number' | 'boolean' | 'string' | 'error'; class TypeError { constructor( public message: string, public location: string ) {}} // ─── Visitor Interface ───────────────────────────────────────── interface ExpressionVisitor<R> { visitNumberLiteral(expr: NumberLiteral): R; visitStringLiteral(expr: StringLiteral): R; visitBooleanLiteral(expr: BooleanLiteral): R; visitBinaryOp(expr: BinaryOp): R; visitUnaryOp(expr: UnaryOp): R; visitVariable(expr: Variable): R; visitConditional(expr: Conditional): R; visitFunctionCall(expr: FunctionCall): R;} // ─── Expression Base and Concrete Types ──────────────────────── abstract class Expression { abstract accept<R>(visitor: ExpressionVisitor<R>): R; abstract toString(): string;} class NumberLiteral extends Expression { constructor(public readonly value: number) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitNumberLiteral(this); } toString(): string { return this.value.toString(); }} class StringLiteral extends Expression { constructor(public readonly value: string) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitStringLiteral(this); } toString(): string { return `"${this.value}"`; }} class BooleanLiteral extends Expression { constructor(public readonly value: boolean) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitBooleanLiteral(this); } toString(): string { return this.value.toString(); }} class BinaryOp extends Expression { constructor( public readonly left: Expression, public readonly operator: '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '>' | '&&' | '||', public readonly right: Expression ) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitBinaryOp(this); } toString(): string { return `(${this.left} ${this.operator} ${this.right})`; }} class UnaryOp extends Expression { constructor( public readonly operator: '-' | '!', public readonly operand: Expression ) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitUnaryOp(this); } toString(): string { return `(${this.operator}${this.operand})`; }} class Variable extends Expression { constructor(public readonly name: string) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitVariable(this); } toString(): string { return this.name; }} class Conditional extends Expression { constructor( public readonly condition: Expression, public readonly thenBranch: Expression, public readonly elseBranch: Expression ) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitConditional(this); } toString(): string { return `(${this.condition} ? ${this.thenBranch} : ${this.elseBranch})`; }} class FunctionCall extends Expression { constructor( public readonly name: string, public readonly args: Expression[] ) { super(); } accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitFunctionCall(this); } toString(): string { return `${this.name}(${this.args.join(', ')})`; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
// ─── Type Checker Visitor ────────────────────────────────────── interface TypeEnvironment { variables: Map<string, ExprType>; functions: Map<string, { params: ExprType[]; returnType: ExprType }>;} class TypeCheckVisitor implements ExpressionVisitor<ExprType> { private errors: TypeError[] = []; constructor(private env: TypeEnvironment) {} visitNumberLiteral(expr: NumberLiteral): ExprType { return 'number'; } visitStringLiteral(expr: StringLiteral): ExprType { return 'string'; } visitBooleanLiteral(expr: BooleanLiteral): ExprType { return 'boolean'; } visitBinaryOp(expr: BinaryOp): ExprType { const leftType = expr.left.accept(this); const rightType = expr.right.accept(this); // Arithmetic operators require numbers if (['+', '-', '*', '/'].includes(expr.operator)) { if (expr.operator === '+' && leftType === 'string' && rightType === 'string') { return 'string'; // String concatenation } if (leftType !== 'number' || rightType !== 'number') { this.errors.push(new TypeError( `Operator '${expr.operator}' requires number operands, got ${leftType} and ${rightType}`, expr.toString() )); return 'error'; } return 'number'; } // Comparison operators if (['<', '>'].includes(expr.operator)) { if (leftType !== 'number' || rightType !== 'number') { this.errors.push(new TypeError( `Comparison requires numbers, got ${leftType} and ${rightType}`, expr.toString() )); return 'error'; } return 'boolean'; } // Equality operators work on any same type if (['==', '!='].includes(expr.operator)) { if (leftType !== rightType) { this.errors.push(new TypeError( `Cannot compare ${leftType} with ${rightType}`, expr.toString() )); return 'error'; } return 'boolean'; } // Logical operators require booleans if (['&&', '||'].includes(expr.operator)) { if (leftType !== 'boolean' || rightType !== 'boolean') { this.errors.push(new TypeError( `Logical operator requires booleans, got ${leftType} and ${rightType}`, expr.toString() )); return 'error'; } return 'boolean'; } return 'error'; } visitUnaryOp(expr: UnaryOp): ExprType { const operandType = expr.operand.accept(this); if (expr.operator === '-') { if (operandType !== 'number') { this.errors.push(new TypeError( `Unary minus requires number, got ${operandType}`, expr.toString() )); return 'error'; } return 'number'; } if (expr.operator === '!') { if (operandType !== 'boolean') { this.errors.push(new TypeError( `Logical not requires boolean, got ${operandType}`, expr.toString() )); return 'error'; } return 'boolean'; } return 'error'; } visitVariable(expr: Variable): ExprType { const type = this.env.variables.get(expr.name); if (!type) { this.errors.push(new TypeError( `Undefined variable: ${expr.name}`, expr.toString() )); return 'error'; } return type; } visitConditional(expr: Conditional): ExprType { const condType = expr.condition.accept(this); const thenType = expr.thenBranch.accept(this); const elseType = expr.elseBranch.accept(this); if (condType !== 'boolean') { this.errors.push(new TypeError( `Condition must be boolean, got ${condType}`, expr.toString() )); } if (thenType !== elseType) { this.errors.push(new TypeError( `Conditional branches must have same type: ${thenType} vs ${elseType}`, expr.toString() )); return 'error'; } return thenType; } visitFunctionCall(expr: FunctionCall): ExprType { const func = this.env.functions.get(expr.name); if (!func) { this.errors.push(new TypeError( `Undefined function: ${expr.name}`, expr.toString() )); return 'error'; } if (expr.args.length !== func.params.length) { this.errors.push(new TypeError( `Function ${expr.name} expects ${func.params.length} args, got ${expr.args.length}`, expr.toString() )); return 'error'; } for (let i = 0; i < expr.args.length; i++) { const argType = expr.args[i].accept(this); if (argType !== func.params[i]) { this.errors.push(new TypeError( `Argument ${i + 1} of ${expr.name}: expected ${func.params[i]}, got ${argType}`, expr.args[i].toString() )); } } return func.returnType; } getErrors(): TypeError[] { return this.errors; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
// ─── Constant Folding Optimizer ──────────────────────────────── class ConstantFoldVisitor implements ExpressionVisitor<Expression> { visitNumberLiteral(expr: NumberLiteral): Expression { return expr; // Already a constant } visitStringLiteral(expr: StringLiteral): Expression { return expr; // Already a constant } visitBooleanLiteral(expr: BooleanLiteral): Expression { return expr; // Already a constant } visitBinaryOp(expr: BinaryOp): Expression { // First, recursively optimize children const left = expr.left.accept(this); const right = expr.right.accept(this); // If both are number literals, fold if (left instanceof NumberLiteral && right instanceof NumberLiteral) { switch (expr.operator) { case '+': return new NumberLiteral(left.value + right.value); case '-': return new NumberLiteral(left.value - right.value); case '*': return new NumberLiteral(left.value * right.value); case '/': return new NumberLiteral(left.value / right.value); case '<': return new BooleanLiteral(left.value < right.value); case '>': return new BooleanLiteral(left.value > right.value); case '==': return new BooleanLiteral(left.value === right.value); case '!=': return new BooleanLiteral(left.value !== right.value); } } // If both are string literals with +, concatenate if (left instanceof StringLiteral && right instanceof StringLiteral) { if (expr.operator === '+') { return new StringLiteral(left.value + right.value); } } // If both are boolean literals if (left instanceof BooleanLiteral && right instanceof BooleanLiteral) { switch (expr.operator) { case '&&': return new BooleanLiteral(left.value && right.value); case '||': return new BooleanLiteral(left.value || right.value); case '==': return new BooleanLiteral(left.value === right.value); case '!=': return new BooleanLiteral(left.value !== right.value); } } // Algebraic simplifications if (left instanceof NumberLiteral) { if (left.value === 0 && expr.operator === '+') return right; // 0 + x = x if (left.value === 1 && expr.operator === '*') return right; // 1 * x = x if (left.value === 0 && expr.operator === '*') return new NumberLiteral(0); } if (right instanceof NumberLiteral) { if (right.value === 0 && expr.operator === '+') return left; // x + 0 = x if (right.value === 0 && expr.operator === '-') return left; // x - 0 = x if (right.value === 1 && expr.operator === '*') return left; // x * 1 = x if (right.value === 0 && expr.operator === '*') return new NumberLiteral(0); } // Boolean short-circuit simplifications if (left instanceof BooleanLiteral) { if (!left.value && expr.operator === '&&') return new BooleanLiteral(false); if (left.value && expr.operator === '||') return new BooleanLiteral(true); } // Can't fold; return rebuilt expression with optimized children return new BinaryOp(left, expr.operator, right); } visitUnaryOp(expr: UnaryOp): Expression { const operand = expr.operand.accept(this); if (operand instanceof NumberLiteral && expr.operator === '-') { return new NumberLiteral(-operand.value); } if (operand instanceof BooleanLiteral && expr.operator === '!') { return new BooleanLiteral(!operand.value); } // Double negation elimination if (expr.operator === '-' && operand instanceof UnaryOp && operand.operator === '-') { return operand.operand; // --x = x } if (expr.operator === '!' && operand instanceof UnaryOp && operand.operator === '!') { return operand.operand; // !!x = x } return new UnaryOp(expr.operator, operand); } visitVariable(expr: Variable): Expression { return expr; // Variables can't be folded (without data flow analysis) } visitConditional(expr: Conditional): Expression { const condition = expr.condition.accept(this); const thenBranch = expr.thenBranch.accept(this); const elseBranch = expr.elseBranch.accept(this); // If condition is constant, eliminate dead branch if (condition instanceof BooleanLiteral) { return condition.value ? thenBranch : elseBranch; } return new Conditional(condition, thenBranch, elseBranch); } visitFunctionCall(expr: FunctionCall): Expression { // Optimize arguments const optimizedArgs = expr.args.map(arg => arg.accept(this)); // Could fold pure functions with constant args (e.g., sqrt(4) -> 2) // Omitted for brevity return new FunctionCall(expr.name, optimizedArgs); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// ─── JavaScript Code Generator ───────────────────────────────── class JavaScriptEmitVisitor implements ExpressionVisitor<string> { visitNumberLiteral(expr: NumberLiteral): string { return expr.value.toString(); } visitStringLiteral(expr: StringLiteral): string { return JSON.stringify(expr.value); // Handles escaping } visitBooleanLiteral(expr: BooleanLiteral): string { return expr.value.toString(); } visitBinaryOp(expr: BinaryOp): string { const left = expr.left.accept(this); const right = expr.right.accept(this); return `(${left} ${expr.operator} ${right})`; } visitUnaryOp(expr: UnaryOp): string { const operand = expr.operand.accept(this); return `(${expr.operator}${operand})`; } visitVariable(expr: Variable): string { return expr.name; } visitConditional(expr: Conditional): string { const cond = expr.condition.accept(this); const thenBranch = expr.thenBranch.accept(this); const elseBranch = expr.elseBranch.accept(this); return `(${cond} ? ${thenBranch} : ${elseBranch})`; } visitFunctionCall(expr: FunctionCall): string { const args = expr.args.map(arg => arg.accept(this)); // Map to JavaScript equivalents const funcMap: Record<string, string> = { 'sqrt': 'Math.sqrt', 'abs': 'Math.abs', 'sin': 'Math.sin', 'cos': 'Math.cos', 'floor': 'Math.floor', 'ceil': 'Math.ceil', }; const jsFunc = funcMap[expr.name] || expr.name; return `${jsFunc}(${args.join(', ')})`; }} // ─── Usage Example ───────────────────────────────────────────── // Expression: (x + 2) * 3 + sqrt(y)const expr = new BinaryOp( new BinaryOp( new BinaryOp(new Variable('x'), '+', new NumberLiteral(2)), '*', new NumberLiteral(3) ), '+', new FunctionCall('sqrt', [new Variable('y')])); // Type checkconst env: TypeEnvironment = { variables: new Map([['x', 'number'], ['y', 'number']]), functions: new Map([['sqrt', { params: ['number'], returnType: 'number' }]])};const typeChecker = new TypeCheckVisitor(env);const exprType = expr.accept(typeChecker);console.log(`Type: ${exprType}`); // Type: numberconsole.log(`Errors: ${typeChecker.getErrors().length}`); // Errors: 0 // Now with a constant: (5 + 2) * 3 + sqrt(16)const constExpr = new BinaryOp( new BinaryOp( new BinaryOp(new NumberLiteral(5), '+', new NumberLiteral(2)), '*', new NumberLiteral(3) ), '+', new FunctionCall('sqrt', [new NumberLiteral(16)])); // Optimizeconst optimizer = new ConstantFoldVisitor();const optimized = constExpr.accept(optimizer);console.log(`Before: ${constExpr}`); // ((5 + 2) * 3) + sqrt(16))console.log(`After: ${optimized}`); // (21 + sqrt(16)) - partially folded // Generate JavaScriptconst emitter = new JavaScriptEmitVisitor();console.log(`JS: ${expr.accept(emitter)}`); // JS: (((x + 2) * 3) + Math.sqrt(y))A document processing system demonstrates Visitor with composite structures. Documents contain nested elements, and operations must traverse the tree.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
// ═══════════════════════════════════════════════════════════════// DOCUMENT PROCESSING SYSTEM// Demonstrates: Composite structure, multiple export formats// ═══════════════════════════════════════════════════════════════ // ─── Visitor Interface ───────────────────────────────────────── interface DocumentVisitor<R> { visitDocument(doc: Document): R; visitSection(section: Section): R; visitParagraph(para: Paragraph): R; visitHeading(heading: Heading): R; visitList(list: List): R; visitListItem(item: ListItem): R; visitCodeBlock(code: CodeBlock): R; visitImage(image: Image): R; visitLink(link: Link): R; visitEmphasis(emphasis: Emphasis): R;} // ─── Document Elements ───────────────────────────────────────── interface DocumentElement { accept<R>(visitor: DocumentVisitor<R>): R;} class Document implements DocumentElement { constructor( public readonly title: string, public readonly sections: Section[] ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitDocument(this); }} class Section implements DocumentElement { constructor( public readonly id: string, public readonly content: DocumentElement[] ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitSection(this); }} class Paragraph implements DocumentElement { constructor(public readonly content: (string | DocumentElement)[]) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitParagraph(this); }} class Heading implements DocumentElement { constructor( public readonly level: 1 | 2 | 3 | 4 | 5 | 6, public readonly text: string ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitHeading(this); }} class List implements DocumentElement { constructor( public readonly ordered: boolean, public readonly items: ListItem[] ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitList(this); }} class ListItem implements DocumentElement { constructor(public readonly content: DocumentElement[]) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitListItem(this); }} class CodeBlock implements DocumentElement { constructor( public readonly language: string, public readonly code: string ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitCodeBlock(this); }} class Image implements DocumentElement { constructor( public readonly src: string, public readonly alt: string, public readonly caption?: string ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitImage(this); }} class Link implements DocumentElement { constructor( public readonly href: string, public readonly text: string ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitLink(this); }} class Emphasis implements DocumentElement { constructor( public readonly type: 'bold' | 'italic' | 'code', public readonly text: string ) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitEmphasis(this); }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// ─── HTML Export Visitor ─────────────────────────────────────── class HtmlExportVisitor implements DocumentVisitor<string> { private indent = 0; private getIndent(): string { return ' '.repeat(this.indent); } visitDocument(doc: Document): string { const sections = doc.sections.map(s => s.accept(this)).join('\n'); return `<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>${this.escapeHtml(doc.title)}</title> <style> body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; } pre { background: #f4f4f4; padding: 1rem; border-radius: 4px; overflow-x: auto; } code { font-family: 'Fira Code', monospace; } img { max-width: 100%; height: auto; } figcaption { text-align: center; font-style: italic; color: #666; } </style></head><body> <h1>${this.escapeHtml(doc.title)}</h1>${sections}</body></html>`; } visitSection(section: Section): string { this.indent++; const content = section.content.map(el => el.accept(this)).join('\n'); this.indent--; return `${this.getIndent()}<section id="${section.id}">\n${content}\n${this.getIndent()}</section>`; } visitParagraph(para: Paragraph): string { const content = para.content.map(c => typeof c === 'string' ? this.escapeHtml(c) : c.accept(this) ).join(''); return `${this.getIndent()}<p>${content}</p>`; } visitHeading(heading: Heading): string { const tag = `h${heading.level}`; return `${this.getIndent()}<${tag}>${this.escapeHtml(heading.text)}</${tag}>`; } visitList(list: List): string { const tag = list.ordered ? 'ol' : 'ul'; this.indent++; const items = list.items.map(item => item.accept(this)).join('\n'); this.indent--; return `${this.getIndent()}<${tag}>\n${items}\n${this.getIndent()}</${tag}>`; } visitListItem(item: ListItem): string { const content = item.content.map(el => el.accept(this)).join(''); return `${this.getIndent()}<li>${content}</li>`; } visitCodeBlock(code: CodeBlock): string { return `${this.getIndent()}<pre><code class="language-${code.language}">${this.escapeHtml(code.code)}</code></pre>`; } visitImage(image: Image): string { const img = `<img src="${image.src}" alt="${this.escapeHtml(image.alt)}" />`; if (image.caption) { return `${this.getIndent()}<figure>\n${this.getIndent()} ${img}\n${this.getIndent()} <figcaption>${this.escapeHtml(image.caption)}</figcaption>\n${this.getIndent()}</figure>`; } return `${this.getIndent()}${img}`; } visitLink(link: Link): string { return `<a href="${link.href}">${this.escapeHtml(link.text)}</a>`; } visitEmphasis(emphasis: Emphasis): string { switch (emphasis.type) { case 'bold': return `<strong>${this.escapeHtml(emphasis.text)}</strong>`; case 'italic': return `<em>${this.escapeHtml(emphasis.text)}</em>`; case 'code': return `<code>${this.escapeHtml(emphasis.text)}</code>`; } } private escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"'); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
// ─── Markdown Export Visitor ─────────────────────────────────── class MarkdownExportVisitor implements DocumentVisitor<string> { visitDocument(doc: Document): string { const sections = doc.sections.map(s => s.accept(this)).join('\n\n'); return `# ${doc.title}\n\n${sections}`; } visitSection(section: Section): string { return section.content.map(el => el.accept(this)).join('\n\n'); } visitParagraph(para: Paragraph): string { return para.content.map(c => typeof c === 'string' ? c : c.accept(this) ).join(''); } visitHeading(heading: Heading): string { const prefix = '#'.repeat(heading.level); return `${prefix} ${heading.text}`; } visitList(list: List): string { return list.items.map((item, index) => { const prefix = list.ordered ? `${index + 1}.` : '-'; const content = item.accept(this); return `${prefix} ${content}`; }).join('\n'); } visitListItem(item: ListItem): string { return item.content.map(el => el.accept(this)).join(''); } visitCodeBlock(code: CodeBlock): string { return `\`\`\`${code.language}\n${code.code}\n\`\`\``; } visitImage(image: Image): string { const md = ``; return image.caption ? `${md}\n\n*${image.caption}*` : md; } visitLink(link: Link): string { return `[${link.text}](${link.href})`; } visitEmphasis(emphasis: Emphasis): string { switch (emphasis.type) { case 'bold': return `**${emphasis.text}**`; case 'italic': return `*${emphasis.text}*`; case 'code': return `\`${emphasis.text}\``; } }} // ─── Statistics Visitor ──────────────────────────────────────── interface DocumentStats { wordCount: number; headingCount: number; imageCount: number; linkCount: number; codeBlockCount: number; sectionCount: number;} class StatisticsVisitor implements DocumentVisitor<void> { private stats: DocumentStats = { wordCount: 0, headingCount: 0, imageCount: 0, linkCount: 0, codeBlockCount: 0, sectionCount: 0 }; getStats(): DocumentStats { return { ...this.stats }; } private countWords(text: string): number { return text.trim().split(/\s+/).filter(w => w.length > 0).length; } visitDocument(doc: Document): void { this.stats.wordCount += this.countWords(doc.title); doc.sections.forEach(s => s.accept(this)); } visitSection(section: Section): void { this.stats.sectionCount++; section.content.forEach(el => el.accept(this)); } visitParagraph(para: Paragraph): void { para.content.forEach(c => { if (typeof c === 'string') { this.stats.wordCount += this.countWords(c); } else { c.accept(this); } }); } visitHeading(heading: Heading): void { this.stats.headingCount++; this.stats.wordCount += this.countWords(heading.text); } visitList(list: List): void { list.items.forEach(item => item.accept(this)); } visitListItem(item: ListItem): void { item.content.forEach(el => el.accept(this)); } visitCodeBlock(code: CodeBlock): void { this.stats.codeBlockCount++; // Don't count code lines as words } visitImage(image: Image): void { this.stats.imageCount++; this.stats.wordCount += this.countWords(image.alt); if (image.caption) { this.stats.wordCount += this.countWords(image.caption); } } visitLink(link: Link): void { this.stats.linkCount++; this.stats.wordCount += this.countWords(link.text); } visitEmphasis(emphasis: Emphasis): void { this.stats.wordCount += this.countWords(emphasis.text); }}File systems are a classic Visitor domain. The type hierarchy is stable (files, directories, symlinks), but operations are numerous (size calculation, search, backup, permission check).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
// ═══════════════════════════════════════════════════════════════// FILE SYSTEM VISITOR EXAMPLE// Demonstrates: Size calculation, search, permission check// ═══════════════════════════════════════════════════════════════ // ─── Visitor Interface ───────────────────────────────────────── interface FileSystemVisitor<R> { visitFile(file: File): R; visitDirectory(dir: Directory): R; visitSymlink(link: Symlink): R;} // ─── File System Nodes ───────────────────────────────────────── interface FileSystemNode { name: string; path: string; accept<R>(visitor: FileSystemVisitor<R>): R;} class File implements FileSystemNode { constructor( public readonly name: string, public readonly path: string, public readonly size: number, public readonly mimeType: string, public readonly permissions: number, public readonly lastModified: Date ) {} accept<R>(visitor: FileSystemVisitor<R>): R { return visitor.visitFile(this); }} class Directory implements FileSystemNode { constructor( public readonly name: string, public readonly path: string, public readonly children: FileSystemNode[], public readonly permissions: number ) {} accept<R>(visitor: FileSystemVisitor<R>): R { return visitor.visitDirectory(this); }} class Symlink implements FileSystemNode { constructor( public readonly name: string, public readonly path: string, public readonly target: string, public readonly targetNode?: FileSystemNode ) {} accept<R>(visitor: FileSystemVisitor<R>): R { return visitor.visitSymlink(this); }} // ─── Size Calculator ─────────────────────────────────────────── class SizeCalculatorVisitor implements FileSystemVisitor<number> { private followSymlinks: boolean; private visited = new Set<string>(); // Avoid infinite loops constructor(options: { followSymlinks?: boolean } = {}) { this.followSymlinks = options.followSymlinks ?? false; } visitFile(file: File): number { return file.size; } visitDirectory(dir: Directory): number { return dir.children.reduce( (total, child) => total + child.accept(this), 0 ); } visitSymlink(link: Symlink): number { if (!this.followSymlinks || !link.targetNode) { return 0; // Symlink itself has negligible size } // Prevent infinite loops from circular symlinks if (this.visited.has(link.target)) { return 0; } this.visited.add(link.target); return link.targetNode.accept(this); }} // ─── File Search Visitor ─────────────────────────────────────── interface SearchCriteria { namePattern?: RegExp; mimeType?: string; minSize?: number; maxSize?: number; modifiedAfter?: Date; modifiedBefore?: Date;} class FileSearchVisitor implements FileSystemVisitor<File[]> { constructor(private criteria: SearchCriteria) {} visitFile(file: File): File[] { if (this.matches(file)) { return [file]; } return []; } visitDirectory(dir: Directory): File[] { return dir.children.flatMap(child => child.accept(this)); } visitSymlink(link: Symlink): File[] { // Don't follow symlinks for search to avoid duplicates return []; } private matches(file: File): boolean { const { namePattern, mimeType, minSize, maxSize, modifiedAfter, modifiedBefore } = this.criteria; if (namePattern && !namePattern.test(file.name)) return false; if (mimeType && file.mimeType !== mimeType) return false; if (minSize !== undefined && file.size < minSize) return false; if (maxSize !== undefined && file.size > maxSize) return false; if (modifiedAfter && file.lastModified < modifiedAfter) return false; if (modifiedBefore && file.lastModified > modifiedBefore) return false; return true; }} // ─── Permission Checker ──────────────────────────────────────── interface PermissionIssue { path: string; issue: string; severity: 'warning' | 'error';} class PermissionCheckVisitor implements FileSystemVisitor<PermissionIssue[]> { constructor(private userId: number, private groupId: number) {} visitFile(file: File): PermissionIssue[] { const issues: PermissionIssue[] = []; // Check for world-writable files if ((file.permissions & 0o002) !== 0) { issues.push({ path: file.path, issue: 'File is world-writable', severity: 'error' }); } // Check for executable files in unusual locations if ((file.permissions & 0o111) !== 0 && !file.path.includes('/bin/')) { issues.push({ path: file.path, issue: 'Executable file outside bin directory', severity: 'warning' }); } return issues; } visitDirectory(dir: Directory): PermissionIssue[] { const issues: PermissionIssue[] = []; // Check for world-writable directories if ((dir.permissions & 0o002) !== 0) { issues.push({ path: dir.path, issue: 'Directory is world-writable', severity: 'error' }); } // Recursively check children return issues.concat( dir.children.flatMap(child => child.accept(this)) ); } visitSymlink(link: Symlink): PermissionIssue[] { // Symlinks don't have meaningful permissions // But we might warn about broken symlinks if (!link.targetNode) { return [{ path: link.path, issue: `Broken symlink pointing to ${link.target}`, severity: 'warning' }]; } return []; }} // ─── Usage ───────────────────────────────────────────────────── const fileSystem = new Directory('root', '/', [ new File('readme.md', '/readme.md', 1024, 'text/markdown', 0o644, new Date()), new Directory('src', '/src', [ new File('index.ts', '/src/index.ts', 5120, 'text/typescript', 0o644, new Date()), new File('app.ts', '/src/app.ts', 3072, 'text/typescript', 0o644, new Date()), ], 0o755), new Directory('dist', '/dist', [ new File('index.js', '/dist/index.js', 10240, 'application/javascript', 0o755, new Date()), ], 0o755), new Symlink('link-to-src', '/link-to-src', '/src'),], 0o755); // Calculate total sizeconst sizeVisitor = new SizeCalculatorVisitor();const totalSize = fileSystem.accept(sizeVisitor);console.log(`Total size: ${totalSize} bytes`); // Find TypeScript filesconst searchVisitor = new FileSearchVisitor({ namePattern: /\.ts$/});const tsFiles = fileSystem.accept(searchVisitor);console.log(`TypeScript files: ${tsFiles.map(f => f.name).join(', ')}`); // Check permissionsconst permissionVisitor = new PermissionCheckVisitor(1000, 1000);const issues = fileSystem.accept(permissionVisitor);console.log(`Permission issues: ${issues.length}`);GUI frameworks often use Visitor for operations that cross-cut the widget hierarchy: rendering, layout calculation, event handling, accessibility, serialization.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
// ═══════════════════════════════════════════════════════════════// GUI WIDGET VISITOR SYSTEM// Demonstrates: Rendering, accessibility, serialization// ═══════════════════════════════════════════════════════════════ // ─── Visitor Interface ───────────────────────────────────────── interface WidgetVisitor<R> { visitButton(widget: Button): R; visitTextField(widget: TextField): R; visitLabel(widget: Label): R; visitPanel(widget: Panel): R; visitCheckbox(widget: Checkbox): R; visitDropdown(widget: Dropdown): R;} // ─── Widget Base and Types ───────────────────────────────────── interface Widget { id: string; visible: boolean; enabled: boolean; accept<R>(visitor: WidgetVisitor<R>): R;} class Button implements Widget { constructor( public readonly id: string, public readonly text: string, public visible: boolean = true, public enabled: boolean = true, public onClick?: () => void ) {} accept<R>(visitor: WidgetVisitor<R>): R { return visitor.visitButton(this); }} class TextField implements Widget { constructor( public readonly id: string, public readonly placeholder: string, public value: string = '', public visible: boolean = true, public enabled: boolean = true, public readonly maxLength?: number ) {} accept<R>(visitor: WidgetVisitor<R>): R { return visitor.visitTextField(this); }} class Label implements Widget { constructor( public readonly id: string, public readonly text: string, public visible: boolean = true, public enabled: boolean = true ) {} accept<R>(visitor: WidgetVisitor<R>): R { return visitor.visitLabel(this); }} class Panel implements Widget { constructor( public readonly id: string, public readonly children: Widget[], public readonly direction: 'horizontal' | 'vertical' = 'vertical', public visible: boolean = true, public enabled: boolean = true ) {} accept<R>(visitor: WidgetVisitor<R>): R { return visitor.visitPanel(this); }} class Checkbox implements Widget { constructor( public readonly id: string, public readonly label: string, public checked: boolean = false, public visible: boolean = true, public enabled: boolean = true ) {} accept<R>(visitor: WidgetVisitor<R>): R { return visitor.visitCheckbox(this); }} class Dropdown implements Widget { constructor( public readonly id: string, public readonly options: string[], public selectedIndex: number = 0, public visible: boolean = true, public enabled: boolean = true ) {} accept<R>(visitor: WidgetVisitor<R>): R { return visitor.visitDropdown(this); }} // ─── HTML Render Visitor ─────────────────────────────────────── class HtmlRenderVisitor implements WidgetVisitor<string> { visitButton(widget: Button): string { const disabled = widget.enabled ? '' : ' disabled'; const hidden = widget.visible ? '' : ' style="display:none"'; return `<button id="${widget.id}"${disabled}${hidden}>${widget.text}</button>`; } visitTextField(widget: TextField): string { const disabled = widget.enabled ? '' : ' disabled'; const hidden = widget.visible ? '' : ' style="display:none"'; const maxLen = widget.maxLength ? ` maxlength="${widget.maxLength}"` : ''; return `<input type="text" id="${widget.id}" placeholder="${widget.placeholder}" value="${widget.value}"${maxLen}${disabled}${hidden}>`; } visitLabel(widget: Label): string { const hidden = widget.visible ? '' : ' style="display:none"'; return `<label id="${widget.id}"${hidden}>${widget.text}</label>`; } visitPanel(widget: Panel): string { const hidden = widget.visible ? '' : ' style="display:none"'; const flexDir = widget.direction === 'horizontal' ? 'row' : 'column'; const children = widget.children.map(c => c.accept(this)).join('\n '); return `<div id="${widget.id}" style="display:flex;flex-direction:${flexDir}"${hidden}>\n ${children}\n</div>`; } visitCheckbox(widget: Checkbox): string { const checked = widget.checked ? ' checked' : ''; const disabled = widget.enabled ? '' : ' disabled'; const hidden = widget.visible ? '' : ' style="display:none"'; return `<label${hidden}><input type="checkbox" id="${widget.id}"${checked}${disabled}> ${widget.label}</label>`; } visitDropdown(widget: Dropdown): string { const disabled = widget.enabled ? '' : ' disabled'; const hidden = widget.visible ? '' : ' style="display:none"'; const options = widget.options.map((opt, i) => `<option${i === widget.selectedIndex ? ' selected' : ''}>${opt}</option>` ).join(''); return `<select id="${widget.id}"${disabled}${hidden}>${options}</select>`; }} // ─── Accessibility Checker ───────────────────────────────────── interface AccessibilityIssue { widgetId: string; issue: string; severity: 'error' | 'warning' | 'info';} class AccessibilityVisitor implements WidgetVisitor<AccessibilityIssue[]> { visitButton(widget: Button): AccessibilityIssue[] { const issues: AccessibilityIssue[] = []; if (!widget.text || widget.text.trim().length === 0) { issues.push({ widgetId: widget.id, issue: 'Button has no text - add aria-label for screen readers', severity: 'error' }); } if (widget.text.length > 50) { issues.push({ widgetId: widget.id, issue: 'Button text is very long - consider shortening', severity: 'warning' }); } return issues; } visitTextField(widget: TextField): AccessibilityIssue[] { const issues: AccessibilityIssue[] = []; if (!widget.placeholder) { issues.push({ widgetId: widget.id, issue: 'Text field has no placeholder - add label for accessibility', severity: 'warning' }); } return issues; } visitLabel(widget: Label): AccessibilityIssue[] { // Labels are accessibility-friendly by nature return []; } visitPanel(widget: Panel): AccessibilityIssue[] { // Check children return widget.children.flatMap(child => child.accept(this)); } visitCheckbox(widget: Checkbox): AccessibilityIssue[] { const issues: AccessibilityIssue[] = []; if (!widget.label) { issues.push({ widgetId: widget.id, issue: 'Checkbox has no label', severity: 'error' }); } return issues; } visitDropdown(widget: Dropdown): AccessibilityIssue[] { const issues: AccessibilityIssue[] = []; if (widget.options.length === 0) { issues.push({ widgetId: widget.id, issue: 'Dropdown has no options', severity: 'error' }); } return issues; }} // ─── State Serializer ────────────────────────────────────────── interface WidgetState { id: string; type: string; visible: boolean; enabled: boolean; [key: string]: unknown;} class StateSerializerVisitor implements WidgetVisitor<WidgetState | WidgetState[]> { visitButton(widget: Button): WidgetState { return { id: widget.id, type: 'button', visible: widget.visible, enabled: widget.enabled, text: widget.text }; } visitTextField(widget: TextField): WidgetState { return { id: widget.id, type: 'textField', visible: widget.visible, enabled: widget.enabled, value: widget.value, placeholder: widget.placeholder }; } visitLabel(widget: Label): WidgetState { return { id: widget.id, type: 'label', visible: widget.visible, enabled: widget.enabled, text: widget.text }; } visitPanel(widget: Panel): WidgetState { const children = widget.children.flatMap(c => { const result = c.accept(this); return Array.isArray(result) ? result : [result]; }); return { id: widget.id, type: 'panel', visible: widget.visible, enabled: widget.enabled, direction: widget.direction, children }; } visitCheckbox(widget: Checkbox): WidgetState { return { id: widget.id, type: 'checkbox', visible: widget.visible, enabled: widget.enabled, label: widget.label, checked: widget.checked }; } visitDropdown(widget: Dropdown): WidgetState { return { id: widget.id, type: 'dropdown', visible: widget.visible, enabled: widget.enabled, options: widget.options, selectedIndex: widget.selectedIndex }; }}These examples demonstrate the Visitor pattern's power across diverse domains. Let's summarize the key insights:
You have now mastered the Visitor pattern. You understand the problem it solves (adding operations without modifying classes), the solution (double dispatch), when to apply it (stable types, volatile operations), and how it works in practice across multiple domains. Apply this knowledge when you encounter stable hierarchies that need extensible operations.