Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Visitor Pattern! ๐ Ever wanted to add new operations to your objects without modifying their classes? Thatโs exactly what the Visitor Pattern lets you do!
Imagine youโre building a document editor that can export to different formats - PDF ๐, HTML ๐, and Markdown ๐. Instead of cramming all that export logic into each document element, the Visitor Pattern lets you keep things clean and extensible!
By the end of this tutorial, youโll be adding new operations to your TypeScript classes like a pro! Letโs dive in! ๐โโ๏ธ
๐ Understanding the Visitor Pattern
๐ค What is the Visitor Pattern?
The Visitor Pattern is like having specialized repair technicians ๐ง visit your house. Each technician (visitor) knows how to work with different appliances (elements), but the appliances donโt need to know how to repair themselves!
In TypeScript terms, itโs a behavioral pattern that lets you separate algorithms from the objects they operate on. This means you can:
- โจ Add new operations without changing existing classes
- ๐ Keep related behaviors together in visitor classes
- ๐ก๏ธ Follow the Open/Closed Principle perfectly
๐ก Why Use the Visitor Pattern?
Hereโs why developers love the Visitor Pattern:
- Separation of Concerns ๐: Operations are separate from data structures
- Easy Extension ๐ป: Add new operations without touching existing code
- Type Safety ๐: TypeScript ensures visitors handle all element types
- Clean Organization ๐ง: Related operations stay together
Real-world example: Imagine building a file system analyzer ๐. With the Visitor Pattern, you can easily add operations like calculating size, counting files, or searching for content without modifying your file and folder classes!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Visitor Pattern!
// ๐จ Element interface - what can be visited
interface Element {
accept(visitor: Visitor): void;
}
// ๐ง Visitor interface - what can visit
interface Visitor {
visitBook(book: Book): void; // ๐ Visit a book
visitMovie(movie: Movie): void; // ๐ฌ Visit a movie
visitGame(game: Game): void; // ๐ฎ Visit a game
}
// ๐ Concrete element: Book
class Book implements Element {
constructor(
public title: string,
public pages: number,
public author: string
) {}
accept(visitor: Visitor): void {
visitor.visitBook(this);
}
}
// ๐ฌ Concrete element: Movie
class Movie implements Element {
constructor(
public title: string,
public duration: number,
public director: string
) {}
accept(visitor: Visitor): void {
visitor.visitMovie(this);
}
}
// ๐ฎ Concrete element: Game
class Game implements Element {
constructor(
public title: string,
public platform: string,
public genre: string
) {}
accept(visitor: Visitor): void {
visitor.visitGame(this);
}
}
๐ก Explanation: Each element knows how to accept a visitor, but doesnโt know what the visitor will do. Thatโs the beauty of separation!
๐ฏ Creating Visitors
Hereโs how to create different visitors:
// ๐ฐ Price calculator visitor
class PriceCalculator implements Visitor {
private total = 0;
visitBook(book: Book): void {
// ๐ Books: $0.10 per page
const price = book.pages * 0.10;
this.total += price;
console.log(`๐ ${book.title}: $${price.toFixed(2)}`);
}
visitMovie(movie: Movie): void {
// ๐ฌ Movies: $15 flat rate
const price = 15;
this.total += price;
console.log(`๐ฌ ${movie.title}: $${price.toFixed(2)}`);
}
visitGame(game: Game): void {
// ๐ฎ Games: $60 for console, $30 for PC
const price = game.platform === "console" ? 60 : 30;
this.total += price;
console.log(`๐ฎ ${game.title}: $${price.toFixed(2)}`);
}
getTotal(): number {
return this.total;
}
}
// ๐ Info display visitor
class InfoDisplayer implements Visitor {
visitBook(book: Book): void {
console.log(`๐ Book: "${book.title}" by ${book.author} (${book.pages} pages)`);
}
visitMovie(movie: Movie): void {
console.log(`๐ฌ Movie: "${movie.title}" directed by ${movie.director} (${movie.duration} min)`);
}
visitGame(game: Game): void {
console.log(`๐ฎ Game: "${game.title}" - ${game.genre} on ${game.platform}`);
}
}
๐ก Practical Examples
๐ Example 1: Shopping Cart Analysis
Letโs build a shopping cart with multiple analysis operations:
// ๐๏ธ Product hierarchy
interface Product extends Element {
name: string;
price: number;
}
class Electronics implements Product {
constructor(
public name: string,
public price: number,
public warranty: number,
public brand: string
) {}
accept(visitor: ShoppingVisitor): void {
visitor.visitElectronics(this);
}
}
class Clothing implements Product {
constructor(
public name: string,
public price: number,
public size: string,
public material: string
) {}
accept(visitor: ShoppingVisitor): void {
visitor.visitClothing(this);
}
}
class Food implements Product {
constructor(
public name: string,
public price: number,
public expiryDate: Date,
public organic: boolean
) {}
accept(visitor: ShoppingVisitor): void {
visitor.visitFood(this);
}
}
// ๐ง Shopping visitor interface
interface ShoppingVisitor extends Visitor {
visitElectronics(item: Electronics): void;
visitClothing(item: Clothing): void;
visitFood(item: Food): void;
}
// ๐ธ Discount calculator visitor
class DiscountCalculator implements ShoppingVisitor {
private discounts: Map<string, number> = new Map();
visitElectronics(item: Electronics): void {
// ๐ฑ 10% off electronics over $500
const discount = item.price > 500 ? item.price * 0.10 : 0;
if (discount > 0) {
this.discounts.set(item.name, discount);
console.log(`๐ธ ${item.name}: Save $${discount.toFixed(2)}!`);
}
}
visitClothing(item: Clothing): void {
// ๐ 20% off summer clothing
const isSummer = item.material === "cotton" || item.material === "linen";
const discount = isSummer ? item.price * 0.20 : 0;
if (discount > 0) {
this.discounts.set(item.name, discount);
console.log(`๐ท๏ธ ${item.name}: Summer sale $${discount.toFixed(2)} off!`);
}
}
visitFood(item: Food): void {
// ๐ฅ 15% off organic food
const discount = item.organic ? item.price * 0.15 : 0;
if (discount > 0) {
this.discounts.set(item.name, discount);
console.log(`๐ฟ ${item.name}: Organic discount $${discount.toFixed(2)}!`);
}
}
getTotalDiscount(): number {
return Array.from(this.discounts.values()).reduce((sum, d) => sum + d, 0);
}
}
// ๐ฆ Shipping calculator visitor
class ShippingCalculator implements ShoppingVisitor {
private totalWeight = 0;
private fragileItems: string[] = [];
visitElectronics(item: Electronics): void {
// ๐ฅ๏ธ Electronics are fragile and heavy
this.totalWeight += 2; // kg
this.fragileItems.push(item.name);
console.log(`โ ๏ธ ${item.name}: Handle with care! ๐ฆ`);
}
visitClothing(item: Clothing): void {
// ๐ Clothing is light
this.totalWeight += 0.5; // kg
console.log(`๐ฆ ${item.name}: Standard packaging`);
}
visitFood(item: Food): void {
// ๐ Food needs special handling
this.totalWeight += 1; // kg
const daysUntilExpiry = Math.floor(
(item.expiryDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
console.log(`๐ ${item.name}: Express shipping (expires in ${daysUntilExpiry} days)`);
}
getShippingCost(): number {
// ๐ต $5 base + $2 per kg + $10 for fragile
const fragileCharge = this.fragileItems.length > 0 ? 10 : 0;
return 5 + (this.totalWeight * 2) + fragileCharge;
}
}
// ๐ฎ Let's use it!
const cart: Product[] = [
new Electronics("Gaming Laptop", 1200, 24, "TechBrand"),
new Clothing("Summer Shirt", 30, "L", "cotton"),
new Food("Organic Apples", 5, new Date("2024-01-15"), true)
];
const discountCalc = new DiscountCalculator();
const shippingCalc = new ShippingCalculator();
console.log("๐ Analyzing your cart...\n");
cart.forEach(item => {
item.accept(discountCalc as any);
item.accept(shippingCalc as any);
console.log("");
});
console.log(`๐ฐ Total discount: $${discountCalc.getTotalDiscount().toFixed(2)}`);
console.log(`๐ฆ Shipping cost: $${shippingCalc.getShippingCost().toFixed(2)}`);
๐ฏ Try it yourself: Add a TaxCalculator
visitor that applies different tax rates based on product type!
๐ฎ Example 2: Game Entity System
Letโs create a game with different entity operations:
// ๐ฎ Game entity system
interface GameEntity extends Element {
name: string;
position: { x: number; y: number };
health: number;
}
class Player implements GameEntity {
constructor(
public name: string,
public position: { x: number; y: number },
public health: number,
public level: number,
public inventory: string[]
) {}
accept(visitor: GameVisitor): void {
visitor.visitPlayer(this);
}
}
class Enemy implements GameEntity {
constructor(
public name: string,
public position: { x: number; y: number },
public health: number,
public damage: number,
public loot: string
) {}
accept(visitor: GameVisitor): void {
visitor.visitEnemy(this);
}
}
class NPC implements GameEntity {
constructor(
public name: string,
public position: { x: number; y: number },
public health: number,
public dialogue: string[],
public questId?: string
) {}
accept(visitor: GameVisitor): void {
visitor.visitNPC(this);
}
}
// ๐ฏ Game visitor interface
interface GameVisitor extends Visitor {
visitPlayer(player: Player): void;
visitEnemy(enemy: Enemy): void;
visitNPC(npc: NPC): void;
}
// ๐บ๏ธ Renderer visitor
class GameRenderer implements GameVisitor {
private screen: string[][] = Array(10).fill(null).map(() => Array(20).fill(' '));
visitPlayer(player: Player): void {
this.screen[player.position.y][player.position.x] = '๐ฆธ';
console.log(`๐ฎ Rendering ${player.name} at (${player.position.x}, ${player.position.y})`);
}
visitEnemy(enemy: Enemy): void {
const enemyIcon = enemy.health > 50 ? '๐น' : '๐';
this.screen[enemy.position.y][enemy.position.x] = enemyIcon;
console.log(`โ๏ธ Rendering ${enemy.name} at (${enemy.position.x}, ${enemy.position.y})`);
}
visitNPC(npc: NPC): void {
const npcIcon = npc.questId ? 'โ' : '๐ง';
this.screen[npc.position.y][npc.position.x] = npcIcon;
console.log(`๐ฌ Rendering ${npc.name} at (${npc.position.x}, ${npc.position.y})`);
}
display(): void {
console.log("\n๐บ๏ธ Game Map:");
console.log("โ" + "โ".repeat(20) + "โ");
this.screen.forEach(row => {
console.log("โ" + row.join('') + "โ");
});
console.log("โ" + "โ".repeat(20) + "โ");
}
}
// ๐ Healer visitor
class HealerVisitor implements GameVisitor {
private healingPower = 50;
visitPlayer(player: Player): void {
const oldHealth = player.health;
player.health = Math.min(100, player.health + this.healingPower);
console.log(`๐ Healed ${player.name}: ${oldHealth} โ ${player.health} HP`);
}
visitEnemy(enemy: Enemy): void {
// ๐ซ Enemies don't get healed!
console.log(`โ Cannot heal enemy ${enemy.name}`);
}
visitNPC(npc: NPC): void {
const oldHealth = npc.health;
npc.health = Math.min(100, npc.health + this.healingPower / 2);
console.log(`๐ Healed ${npc.name}: ${oldHealth} โ ${npc.health} HP`);
}
}
// ๐ Stats collector visitor
class StatsCollector implements GameVisitor {
private stats = {
totalEntities: 0,
totalHealth: 0,
players: 0,
enemies: 0,
npcs: 0,
questGivers: 0
};
visitPlayer(player: Player): void {
this.stats.totalEntities++;
this.stats.totalHealth += player.health;
this.stats.players++;
console.log(`๐ Player ${player.name}: Level ${player.level}, ${player.inventory.length} items`);
}
visitEnemy(enemy: Enemy): void {
this.stats.totalEntities++;
this.stats.totalHealth += enemy.health;
this.stats.enemies++;
console.log(`๐ Enemy ${enemy.name}: ${enemy.damage} damage, drops ${enemy.loot}`);
}
visitNPC(npc: NPC): void {
this.stats.totalEntities++;
this.stats.totalHealth += npc.health;
this.stats.npcs++;
if (npc.questId) {
this.stats.questGivers++;
console.log(`๐ Quest NPC ${npc.name}: Quest ID ${npc.questId}`);
} else {
console.log(`๐ NPC ${npc.name}: ${npc.dialogue.length} dialogue lines`);
}
}
displayStats(): void {
console.log("\n๐ Game Statistics:");
console.log(` ๐ฅ Total Entities: ${this.stats.totalEntities}`);
console.log(` ๐ Average Health: ${(this.stats.totalHealth / this.stats.totalEntities).toFixed(1)}`);
console.log(` ๐ฆธ Players: ${this.stats.players}`);
console.log(` ๐น Enemies: ${this.stats.enemies}`);
console.log(` ๐ง NPCs: ${this.stats.npcs} (${this.stats.questGivers} with quests)`);
}
}
// ๐ฎ Create game world
const gameEntities: GameEntity[] = [
new Player("Hero", { x: 5, y: 5 }, 75, 10, ["Sword", "Shield", "Potion"]),
new Enemy("Goblin", { x: 15, y: 3 }, 30, 15, "Gold Coins"),
new Enemy("Dragon", { x: 18, y: 8 }, 100, 50, "Dragon Scale"),
new NPC("Wizard", { x: 10, y: 2 }, 100, ["Welcome, hero!", "I have a quest for you."], "QUEST_001"),
new NPC("Villager", { x: 3, y: 7 }, 100, ["Nice weather today!", "Have you seen any goblins?"])
];
// ๐ฏ Apply different operations
const renderer = new GameRenderer();
const healer = new HealerVisitor();
const stats = new StatsCollector();
console.log("๐ฎ === GAME WORLD OPERATIONS ===\n");
// Render all entities
gameEntities.forEach(entity => entity.accept(renderer as any));
renderer.display();
console.log("\n๐ === HEALING ROUND ===");
gameEntities.forEach(entity => entity.accept(healer as any));
console.log("\n๐ === STATISTICS ===");
gameEntities.forEach(entity => entity.accept(stats as any));
stats.displayStats();
๐ Advanced Concepts
๐งโโ๏ธ Double Dispatch Magic
The Visitor Pattern uses double dispatch to achieve its magic:
// ๐ฏ Advanced visitor with type safety
abstract class AdvancedElement {
abstract accept<T>(visitor: AdvancedVisitor<T>): T;
}
abstract class AdvancedVisitor<T> {
abstract visitCircle(circle: Circle): T;
abstract visitRectangle(rectangle: Rectangle): T;
abstract visitTriangle(triangle: Triangle): T;
}
// ๐ข Circle implementation
class Circle extends AdvancedElement {
constructor(public radius: number) {
super();
}
accept<T>(visitor: AdvancedVisitor<T>): T {
return visitor.visitCircle(this); // โจ Double dispatch!
}
}
// ๐ฆ Rectangle implementation
class Rectangle extends AdvancedElement {
constructor(public width: number, public height: number) {
super();
}
accept<T>(visitor: AdvancedVisitor<T>): T {
return visitor.visitRectangle(this);
}
}
// ๐บ Triangle implementation
class Triangle extends AdvancedElement {
constructor(public base: number, public height: number) {
super();
}
accept<T>(visitor: AdvancedVisitor<T>): T {
return visitor.visitTriangle(this);
}
}
// ๐ Area calculator with return values
class AreaCalculator extends AdvancedVisitor<number> {
visitCircle(circle: Circle): number {
const area = Math.PI * circle.radius ** 2;
console.log(`๐ข Circle area: ${area.toFixed(2)}`);
return area;
}
visitRectangle(rectangle: Rectangle): number {
const area = rectangle.width * rectangle.height;
console.log(`๐ฆ Rectangle area: ${area.toFixed(2)}`);
return area;
}
visitTriangle(triangle: Triangle): number {
const area = (triangle.base * triangle.height) / 2;
console.log(`๐บ Triangle area: ${area.toFixed(2)}`);
return area;
}
}
// ๐จ SVG generator visitor
class SVGGenerator extends AdvancedVisitor<string> {
visitCircle(circle: Circle): string {
return `<circle r="${circle.radius}" fill="green" />`;
}
visitRectangle(rectangle: Rectangle): string {
return `<rect width="${rectangle.width}" height="${rectangle.height}" fill="blue" />`;
}
visitTriangle(triangle: Triangle): string {
const points = `0,${triangle.height} ${triangle.base/2},0 ${triangle.base},${triangle.height}`;
return `<polygon points="${points}" fill="red" />`;
}
}
๐๏ธ Composite Visitor Pattern
Combine with Composite Pattern for tree structures:
// ๐ณ File system with visitor support
interface FileSystemNode {
name: string;
accept(visitor: FileSystemVisitor): void;
}
class File implements FileSystemNode {
constructor(
public name: string,
public size: number,
public extension: string
) {}
accept(visitor: FileSystemVisitor): void {
visitor.visitFile(this);
}
}
class Directory implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(public name: string) {}
add(node: FileSystemNode): void {
this.children.push(node);
}
accept(visitor: FileSystemVisitor): void {
visitor.visitDirectory(this);
// ๐ Visit all children
this.children.forEach(child => child.accept(visitor));
}
}
interface FileSystemVisitor {
visitFile(file: File): void;
visitDirectory(directory: Directory): void;
}
// ๐ Size calculator visitor
class SizeCalculator implements FileSystemVisitor {
private stack: number[] = [0];
visitFile(file: File): void {
this.stack[this.stack.length - 1] += file.size;
console.log(`๐ ${file.name}.${file.extension}: ${file.size} KB`);
}
visitDirectory(directory: Directory): void {
console.log(`๐ Entering ${directory.name}/`);
this.stack.push(0); // Push new counter for this directory
}
getTotalSize(): number {
return this.stack[0];
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Breaking Encapsulation
// โ Wrong way - exposing too much!
class BadElement {
private secretData = "Don't expose me!";
accept(visitor: any): void {
visitor.visit(this.secretData); // ๐ฅ Breaking encapsulation!
}
}
// โ
Correct way - controlled access!
class GoodElement {
private secretData = "I'm safe!";
accept(visitor: ElementVisitor): void {
visitor.visitElement(this.getPublicData());
}
getPublicData(): string {
return "Public information only"; // โ
Encapsulation preserved!
}
}
๐คฏ Pitfall 2: Circular Dependencies
// โ Dangerous - circular dependency!
// File: element.ts
import { Visitor } from './visitor'; // ๐ฅ Circular!
// File: visitor.ts
import { Element } from './element'; // ๐ฅ Circular!
// โ
Safe - use interfaces!
// File: interfaces.ts
export interface IElement {
accept(visitor: IVisitor): void;
}
export interface IVisitor {
visitElement(element: IElement): void;
}
// File: element.ts
import { IElement, IVisitor } from './interfaces';
class Element implements IElement {
accept(visitor: IVisitor): void {
visitor.visitElement(this); // โ
No circular dependency!
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep Visitors Focused: Each visitor should have one responsibility
- ๐ Use Clear Naming:
visitX
methods should clearly indicate what X is - ๐ก๏ธ Type Safety First: Leverage TypeScriptโs type system fully
- ๐จ Avoid State in Elements: Let visitors manage their own state
- โจ Consider Return Values: Visitors can return results for functional style
๐งช Hands-On Exercise
๐ฏ Challenge: Build an Expression Evaluator
Create a mathematical expression evaluator using the Visitor Pattern:
๐ Requirements:
- โ Support numbers, addition, multiplication, and variables
- ๐ท๏ธ Different visitors for evaluation, printing, and simplification
- ๐ค Variable substitution capability
- ๐ Expression validation
- ๐จ Pretty printing with proper parentheses
๐ Bonus Points:
- Add support for more operations (subtraction, division)
- Implement derivative calculation visitor
- Create an expression optimizer
๐ก Solution
๐ Click to see solution
// ๐ฏ Expression evaluator with Visitor Pattern!
// ๐ณ Expression hierarchy
interface Expression {
accept<T>(visitor: ExpressionVisitor<T>): T;
}
class NumberExpr implements Expression {
constructor(public value: number) {}
accept<T>(visitor: ExpressionVisitor<T>): T {
return visitor.visitNumber(this);
}
}
class VariableExpr implements Expression {
constructor(public name: string) {}
accept<T>(visitor: ExpressionVisitor<T>): T {
return visitor.visitVariable(this);
}
}
class BinaryExpr implements Expression {
constructor(
public left: Expression,
public operator: '+' | '*' | '-' | '/',
public right: Expression
) {}
accept<T>(visitor: ExpressionVisitor<T>): T {
return visitor.visitBinary(this);
}
}
// ๐ง Visitor interface
interface ExpressionVisitor<T> {
visitNumber(expr: NumberExpr): T;
visitVariable(expr: VariableExpr): T;
visitBinary(expr: BinaryExpr): T;
}
// ๐งฎ Evaluator visitor
class EvaluatorVisitor implements ExpressionVisitor<number> {
constructor(private variables: Map<string, number> = new Map()) {}
visitNumber(expr: NumberExpr): number {
return expr.value;
}
visitVariable(expr: VariableExpr): number {
const value = this.variables.get(expr.name);
if (value === undefined) {
throw new Error(`๐ซ Undefined variable: ${expr.name}`);
}
return value;
}
visitBinary(expr: BinaryExpr): number {
const left = expr.left.accept(this);
const right = expr.right.accept(this);
switch (expr.operator) {
case '+': return left + right;
case '*': return left * right;
case '-': return left - right;
case '/':
if (right === 0) throw new Error('๐ฅ Division by zero!');
return left / right;
}
}
}
// ๐ Pretty printer visitor
class PrettyPrinterVisitor implements ExpressionVisitor<string> {
visitNumber(expr: NumberExpr): string {
return expr.value.toString();
}
visitVariable(expr: VariableExpr): string {
return expr.name;
}
visitBinary(expr: BinaryExpr): string {
const left = expr.left.accept(this);
const right = expr.right.accept(this);
// ๐จ Add parentheses for clarity
const needsParens = expr.left instanceof BinaryExpr &&
this.precedence(expr.left.operator) < this.precedence(expr.operator);
const leftStr = needsParens ? `(${left})` : left;
return `${leftStr} ${expr.operator} ${right}`;
}
private precedence(op: string): number {
switch (op) {
case '+': case '-': return 1;
case '*': case '/': return 2;
default: return 0;
}
}
}
// ๐ง Simplifier visitor
class SimplifierVisitor implements ExpressionVisitor<Expression> {
visitNumber(expr: NumberExpr): Expression {
return expr;
}
visitVariable(expr: VariableExpr): Expression {
return expr;
}
visitBinary(expr: BinaryExpr): Expression {
const left = expr.left.accept(this);
const right = expr.right.accept(this);
// ๐ฏ Simplification rules
if (left instanceof NumberExpr && right instanceof NumberExpr) {
// Both operands are numbers - evaluate!
const evaluator = new EvaluatorVisitor();
const result = expr.accept(evaluator);
console.log(`โจ Simplified ${expr.left.value} ${expr.operator} ${expr.right.value} = ${result}`);
return new NumberExpr(result);
}
// Identity rules
if (expr.operator === '+' && right instanceof NumberExpr && right.value === 0) {
console.log(`โจ Simplified x + 0 = x`);
return left;
}
if (expr.operator === '*' && right instanceof NumberExpr && right.value === 1) {
console.log(`โจ Simplified x * 1 = x`);
return left;
}
return new BinaryExpr(left, expr.operator, right);
}
}
// ๐ฎ Test the system!
console.log("๐งฎ === EXPRESSION EVALUATOR ===\n");
// Build expression: (x + 2) * (y - 1)
const expr = new BinaryExpr(
new BinaryExpr(
new VariableExpr("x"),
'+',
new NumberExpr(2)
),
'*',
new BinaryExpr(
new VariableExpr("y"),
'-',
new NumberExpr(1)
)
);
// ๐ Pretty print
const printer = new PrettyPrinterVisitor();
console.log(`๐ Expression: ${expr.accept(printer)}`);
// ๐งฎ Evaluate with variables
const evaluator = new EvaluatorVisitor(new Map([
['x', 5],
['y', 3]
]));
console.log(`๐งฎ With x=5, y=3: ${expr.accept(evaluator)}`);
// ๐ง Simplify
console.log("\nโจ Simplifying expression...");
const simplifier = new SimplifierVisitor();
const simpleExpr = new BinaryExpr(
new NumberExpr(5),
'+',
new BinaryExpr(
new NumberExpr(3),
'*',
new NumberExpr(0)
)
);
console.log(`๐ Original: ${simpleExpr.accept(printer)}`);
const simplified = simpleExpr.accept(simplifier);
console.log(`๐ Simplified: ${simplified.accept(printer)}`);
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create visitors that add operations without modifying classes ๐ช
- โ Avoid common mistakes like breaking encapsulation ๐ก๏ธ
- โ Apply the pattern to real-world scenarios ๐ฏ
- โ Debug visitor issues with confidence ๐
- โ Build extensible systems with TypeScript! ๐
Remember: The Visitor Pattern is perfect when you need to add many operations to a stable class hierarchy. Itโs your Swiss Army knife for separation of concerns! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Visitor Pattern!
Hereโs what to do next:
- ๐ป Practice with the expression evaluator exercise
- ๐๏ธ Apply the pattern to your own projectโs domain objects
- ๐ Move on to our next tutorial: Memento Pattern for state management
- ๐ Combine Visitor with other patterns like Composite or Iterator!
Remember: Design patterns are tools in your toolbox. The Visitor Pattern shines when you need to add operations to existing classes without modification. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ