+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 305 of 354

๐Ÿ“˜ Visitor Pattern: Operation Addition

Master visitor pattern: operation addition in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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:

  1. Separation of Concerns ๐Ÿ”’: Operations are separate from data structures
  2. Easy Extension ๐Ÿ’ป: Add new operations without touching existing code
  3. Type Safety ๐Ÿ“–: TypeScript ensures visitors handle all element types
  4. 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

  1. ๐ŸŽฏ Keep Visitors Focused: Each visitor should have one responsibility
  2. ๐Ÿ“ Use Clear Naming: visitX methods should clearly indicate what X is
  3. ๐Ÿ›ก๏ธ Type Safety First: Leverage TypeScriptโ€™s type system fully
  4. ๐ŸŽจ Avoid State in Elements: Let visitors manage their own state
  5. โœจ 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:

  1. ๐Ÿ’ป Practice with the expression evaluator exercise
  2. ๐Ÿ—๏ธ Apply the pattern to your own projectโ€™s domain objects
  3. ๐Ÿ“š Move on to our next tutorial: Memento Pattern for state management
  4. ๐ŸŒŸ 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! ๐ŸŽ‰๐Ÿš€โœจ