+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 302 of 355

๐Ÿ“˜ State Pattern: Behavioral States

Master state pattern: behavioral states 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 โœจ

๐Ÿ“˜ State Pattern: Behavioral States

๐ŸŽฏ Introduction

Ever played a video game where your character can walk, run, jump, or swim? ๐ŸŽฎ Each of these is a different state, and your character behaves differently in each one. Thatโ€™s exactly what the State Pattern is all about โ€“ managing objects that change their behavior based on their internal state!

The State Pattern is like a chameleon ๐ŸฆŽ that changes its behavior based on its environment. Instead of having massive if-else statements checking conditions everywhere, we elegantly encapsulate each stateโ€™s behavior in its own class. Letโ€™s dive in and make your code more flexible and maintainable! ๐Ÿ’ช

๐Ÿ“š Understanding State Pattern

The State Pattern allows an object to alter its behavior when its internal state changes. Think of it like a traffic light ๐Ÿšฆ:

  • Red state: Cars stop, pedestrians can walk
  • Yellow state: Cars prepare to stop, pedestrians wait
  • Green state: Cars go, pedestrians must wait

Each state has its own set of behaviors, and the light transitions between states based on specific rules. Hereโ€™s the beauty: instead of one massive class handling all possible states, we create separate classes for each state! ๐ŸŽจ

Core Components:

  1. Context: The main object whose behavior changes (the traffic light itself)
  2. State Interface: Defines methods all states must implement
  3. Concrete States: Individual state implementations (Red, Yellow, Green)
  4. State Transitions: Rules for moving between states

๐Ÿ”ง Basic Syntax and Usage

Letโ€™s start with a simple example โ€“ a music player that can be in different states! ๐ŸŽต

// ๐ŸŽฏ State interface
interface PlayerState {
  play(): void;
  pause(): void;
  stop(): void;
}

// ๐ŸŽฎ Context class
class MusicPlayer {
  private state: PlayerState;
  
  constructor() {
    // ๐Ÿ Start in stopped state
    this.state = new StoppedState(this);
  }
  
  setState(state: PlayerState): void {
    this.state = state;
  }
  
  // ๐ŸŽต Delegate actions to current state
  play(): void {
    this.state.play();
  }
  
  pause(): void {
    this.state.pause();
  }
  
  stop(): void {
    this.state.stop();
  }
}

// ๐Ÿ›‘ Stopped state
class StoppedState implements PlayerState {
  constructor(private player: MusicPlayer) {}
  
  play(): void {
    console.log("โ–ถ๏ธ Starting playback!");
    this.player.setState(new PlayingState(this.player));
  }
  
  pause(): void {
    console.log("โš ๏ธ Can't pause - already stopped!");
  }
  
  stop(): void {
    console.log("๐Ÿ›‘ Already stopped!");
  }
}

// ๐ŸŽต Playing state
class PlayingState implements PlayerState {
  constructor(private player: MusicPlayer) {}
  
  play(): void {
    console.log("๐ŸŽต Already playing!");
  }
  
  pause(): void {
    console.log("โธ๏ธ Pausing playback!");
    this.player.setState(new PausedState(this.player));
  }
  
  stop(): void {
    console.log("โน๏ธ Stopping playback!");
    this.player.setState(new StoppedState(this.player));
  }
}

// โธ๏ธ Paused state
class PausedState implements PlayerState {
  constructor(private player: MusicPlayer) {}
  
  play(): void {
    console.log("โ–ถ๏ธ Resuming playback!");
    this.player.setState(new PlayingState(this.player));
  }
  
  pause(): void {
    console.log("โธ๏ธ Already paused!");
  }
  
  stop(): void {
    console.log("โน๏ธ Stopping from pause!");
    this.player.setState(new StoppedState(this.player));
  }
}

// ๐ŸŽช Let's use it!
const player = new MusicPlayer();
player.play();   // โ–ถ๏ธ Starting playback!
player.pause();  // โธ๏ธ Pausing playback!
player.play();   // โ–ถ๏ธ Resuming playback!
player.stop();   // โน๏ธ Stopping playback!

๐Ÿ’ก Practical Examples

Example 1: Smart Door Lock ๐Ÿšช

Letโ€™s build a smart door lock system that can be locked, unlocked, or in alarm mode!

// ๐Ÿ” Door lock states
interface DoorState {
  lock(): void;
  unlock(pin: string): void;
  alarm(): void;
}

class SmartDoor {
  private state: DoorState;
  private correctPin = "1234";
  private attempts = 0;
  
  constructor() {
    this.state = new LockedState(this);
  }
  
  setState(state: DoorState): void {
    this.state = state;
  }
  
  getPin(): string {
    return this.correctPin;
  }
  
  incrementAttempts(): void {
    this.attempts++;
  }
  
  resetAttempts(): void {
    this.attempts = 0;
  }
  
  getAttempts(): number {
    return this.attempts;
  }
  
  // ๐ŸŽฏ Public methods
  lock(): void {
    this.state.lock();
  }
  
  unlock(pin: string): void {
    this.state.unlock(pin);
  }
  
  alarm(): void {
    this.state.alarm();
  }
}

// ๐Ÿ”’ Locked state
class LockedState implements DoorState {
  constructor(private door: SmartDoor) {}
  
  lock(): void {
    console.log("๐Ÿ”’ Door is already locked!");
  }
  
  unlock(pin: string): void {
    if (pin === this.door.getPin()) {
      console.log("โœ… Correct PIN! Door unlocked! ๐Ÿ”“");
      this.door.resetAttempts();
      this.door.setState(new UnlockedState(this.door));
    } else {
      this.door.incrementAttempts();
      console.log(`โŒ Wrong PIN! Attempt ${this.door.getAttempts()}/3`);
      
      if (this.door.getAttempts() >= 3) {
        console.log("๐Ÿšจ Too many attempts! ALARM!");
        this.door.setState(new AlarmState(this.door));
      }
    }
  }
  
  alarm(): void {
    console.log("๐Ÿšจ Triggering alarm!");
    this.door.setState(new AlarmState(this.door));
  }
}

// ๐Ÿ”“ Unlocked state
class UnlockedState implements DoorState {
  constructor(private door: SmartDoor) {}
  
  lock(): void {
    console.log("๐Ÿ”’ Locking door!");
    this.door.setState(new LockedState(this.door));
  }
  
  unlock(pin: string): void {
    console.log("๐Ÿ”“ Door is already unlocked!");
  }
  
  alarm(): void {
    console.log("๐Ÿšจ Triggering alarm!");
    this.door.setState(new AlarmState(this.door));
  }
}

// ๐Ÿšจ Alarm state
class AlarmState implements DoorState {
  constructor(private door: SmartDoor) {}
  
  lock(): void {
    console.log("โš ๏ธ Cannot lock - alarm is active!");
  }
  
  unlock(pin: string): void {
    if (pin === this.door.getPin()) {
      console.log("โœ… Alarm disabled! Door unlocked! ๐Ÿ”“");
      this.door.resetAttempts();
      this.door.setState(new UnlockedState(this.door));
    } else {
      console.log("โŒ Wrong PIN! Alarm still active! ๐Ÿšจ");
    }
  }
  
  alarm(): void {
    console.log("๐Ÿšจ Alarm is already active!");
  }
}

// ๐ŸŽฎ Let's test our smart door!
const door = new SmartDoor();
door.unlock("0000");  // โŒ Wrong PIN! Attempt 1/3
door.unlock("1111");  // โŒ Wrong PIN! Attempt 2/3
door.unlock("2222");  // โŒ Wrong PIN! Attempt 3/3
                      // ๐Ÿšจ Too many attempts! ALARM!
door.lock();          // โš ๏ธ Cannot lock - alarm is active!
door.unlock("1234");  // โœ… Alarm disabled! Door unlocked! ๐Ÿ”“
door.lock();          // ๐Ÿ”’ Locking door!

Example 2: Vending Machine ๐Ÿฅค

Letโ€™s create a vending machine with different states!

// ๐Ÿฅค Vending machine states
interface VendingState {
  insertCoin(): void;
  selectProduct(): void;
  dispense(): void;
  refund(): void;
}

class VendingMachine {
  private state: VendingState;
  private balance = 0;
  private productPrice = 2.50;
  
  constructor() {
    this.state = new IdleState(this);
  }
  
  setState(state: VendingState): void {
    this.state = state;
  }
  
  addBalance(amount: number): void {
    this.balance += amount;
    console.log(`๐Ÿ’ฐ Balance: $${this.balance.toFixed(2)}`);
  }
  
  getBalance(): number {
    return this.balance;
  }
  
  getProductPrice(): number {
    return this.productPrice;
  }
  
  resetBalance(): void {
    this.balance = 0;
  }
  
  // ๐ŸŽฏ Public methods
  insertCoin(): void {
    this.state.insertCoin();
  }
  
  selectProduct(): void {
    this.state.selectProduct();
  }
  
  dispense(): void {
    this.state.dispense();
  }
  
  refund(): void {
    this.state.refund();
  }
}

// ๐Ÿ”„ Idle state
class IdleState implements VendingState {
  constructor(private machine: VendingMachine) {}
  
  insertCoin(): void {
    console.log("๐Ÿช™ Coin inserted!");
    this.machine.addBalance(1.00);
    this.machine.setState(new HasMoneyState(this.machine));
  }
  
  selectProduct(): void {
    console.log("โŒ Please insert money first!");
  }
  
  dispense(): void {
    console.log("โŒ Please insert money and select a product!");
  }
  
  refund(): void {
    console.log("๐Ÿ’ธ Nothing to refund!");
  }
}

// ๐Ÿ’ฐ Has money state
class HasMoneyState implements VendingState {
  constructor(private machine: VendingMachine) {}
  
  insertCoin(): void {
    console.log("๐Ÿช™ Another coin inserted!");
    this.machine.addBalance(1.00);
  }
  
  selectProduct(): void {
    if (this.machine.getBalance() >= this.machine.getProductPrice()) {
      console.log("โœ… Product selected! Dispensing...");
      this.machine.setState(new DispensingState(this.machine));
      this.machine.dispense();
    } else {
      const needed = this.machine.getProductPrice() - this.machine.getBalance();
      console.log(`โŒ Not enough money! Need $${needed.toFixed(2)} more`);
    }
  }
  
  dispense(): void {
    console.log("โŒ Please select a product first!");
  }
  
  refund(): void {
    console.log(`๐Ÿ’ธ Refunding $${this.machine.getBalance().toFixed(2)}`);
    this.machine.resetBalance();
    this.machine.setState(new IdleState(this.machine));
  }
}

// ๐Ÿ“ฆ Dispensing state
class DispensingState implements VendingState {
  constructor(private machine: VendingMachine) {}
  
  insertCoin(): void {
    console.log("โณ Please wait... dispensing product!");
  }
  
  selectProduct(): void {
    console.log("โณ Already dispensing a product!");
  }
  
  dispense(): void {
    console.log("๐Ÿฅค Here's your drink! Enjoy! ๐ŸŽ‰");
    const change = this.machine.getBalance() - this.machine.getProductPrice();
    if (change > 0) {
      console.log(`๐Ÿ’ฐ Your change: $${change.toFixed(2)}`);
    }
    this.machine.resetBalance();
    this.machine.setState(new IdleState(this.machine));
  }
  
  refund(): void {
    console.log("โณ Cannot refund while dispensing!");
  }
}

// ๐ŸŽฎ Let's buy a drink!
const vendingMachine = new VendingMachine();
vendingMachine.selectProduct();  // โŒ Please insert money first!
vendingMachine.insertCoin();      // ๐Ÿช™ Coin inserted! ๐Ÿ’ฐ Balance: $1.00
vendingMachine.insertCoin();      // ๐Ÿช™ Another coin inserted! ๐Ÿ’ฐ Balance: $2.00
vendingMachine.selectProduct();   // โŒ Not enough money! Need $0.50 more
vendingMachine.insertCoin();      // ๐Ÿช™ Another coin inserted! ๐Ÿ’ฐ Balance: $3.00
vendingMachine.selectProduct();   // โœ… Product selected! Dispensing...
                                  // ๐Ÿฅค Here's your drink! Enjoy! ๐ŸŽ‰
                                  // ๐Ÿ’ฐ Your change: $0.50

๐Ÿš€ Advanced Concepts

State with Context Data ๐Ÿ“Š

Sometimes states need to carry data between transitions:

// ๐ŸŽฎ Game character with stats
interface CharacterState {
  move(): void;
  attack(): void;
  takeDamage(damage: number): void;
  heal(): void;
}

class GameCharacter {
  private state: CharacterState;
  private health = 100;
  private maxHealth = 100;
  
  constructor(public name: string) {
    this.state = new HealthyState(this);
  }
  
  setState(state: CharacterState): void {
    this.state = state;
  }
  
  getHealth(): number {
    return this.health;
  }
  
  setHealth(health: number): void {
    this.health = Math.max(0, Math.min(health, this.maxHealth));
    console.log(`โค๏ธ ${this.name}'s health: ${this.health}/${this.maxHealth}`);
    
    // ๐Ÿ”„ Auto-transition based on health
    if (this.health === 0) {
      this.setState(new DefeatedState(this));
    } else if (this.health <= 30) {
      this.setState(new CriticalState(this));
    } else if (this.health <= 60) {
      this.setState(new InjuredState(this));
    } else {
      this.setState(new HealthyState(this));
    }
  }
  
  // ๐ŸŽฏ Actions
  move(): void { this.state.move(); }
  attack(): void { this.state.attack(); }
  takeDamage(damage: number): void { this.state.takeDamage(damage); }
  heal(): void { this.state.heal(); }
}

// ๐Ÿ’ช Healthy state
class HealthyState implements CharacterState {
  constructor(private character: GameCharacter) {}
  
  move(): void {
    console.log(`๐Ÿƒ ${this.character.name} moves swiftly!`);
  }
  
  attack(): void {
    console.log(`โš”๏ธ ${this.character.name} attacks with full power!`);
  }
  
  takeDamage(damage: number): void {
    console.log(`๐Ÿ’ฅ ${this.character.name} takes ${damage} damage!`);
    this.character.setHealth(this.character.getHealth() - damage);
  }
  
  heal(): void {
    console.log(`โœจ ${this.character.name} heals!`);
    this.character.setHealth(this.character.getHealth() + 30);
  }
}

// ๐Ÿค• Injured state
class InjuredState implements CharacterState {
  constructor(private character: GameCharacter) {}
  
  move(): void {
    console.log(`๐Ÿšถ ${this.character.name} limps slowly...`);
  }
  
  attack(): void {
    console.log(`๐Ÿ—ก๏ธ ${this.character.name} attacks weakly`);
  }
  
  takeDamage(damage: number): void {
    console.log(`๐Ÿ’ฅ ${this.character.name} takes ${damage} damage! Ouch!`);
    this.character.setHealth(this.character.getHealth() - damage);
  }
  
  heal(): void {
    console.log(`๐Ÿ’Š ${this.character.name} desperately heals!`);
    this.character.setHealth(this.character.getHealth() + 30);
  }
}

// ๐Ÿ†˜ Critical state
class CriticalState implements CharacterState {
  constructor(private character: GameCharacter) {}
  
  move(): void {
    console.log(`๐ŸŒ ${this.character.name} can barely move!`);
  }
  
  attack(): void {
    console.log(`๐Ÿ˜ฐ ${this.character.name} is too weak to attack!`);
  }
  
  takeDamage(damage: number): void {
    console.log(`๐Ÿ’ฅ ${this.character.name} takes ${damage} critical damage!`);
    this.character.setHealth(this.character.getHealth() - damage);
  }
  
  heal(): void {
    console.log(`๐Ÿ’‰ ${this.character.name} uses emergency healing!`);
    this.character.setHealth(this.character.getHealth() + 40);
  }
}

// ๐Ÿ’€ Defeated state
class DefeatedState implements CharacterState {
  constructor(private character: GameCharacter) {}
  
  move(): void {
    console.log(`โ˜ ๏ธ ${this.character.name} cannot move... defeated!`);
  }
  
  attack(): void {
    console.log(`โ˜ ๏ธ ${this.character.name} cannot attack... defeated!`);
  }
  
  takeDamage(damage: number): void {
    console.log(`โ˜ ๏ธ ${this.character.name} is already defeated!`);
  }
  
  heal(): void {
    console.log(`๐Ÿ”ฎ ${this.character.name} is revived!`);
    this.character.setHealth(50);
  }
}

// ๐ŸŽฎ Epic battle!
const hero = new GameCharacter("Hero");
hero.attack();        // โš”๏ธ Hero attacks with full power!
hero.takeDamage(50);  // ๐Ÿ’ฅ Hero takes 50 damage! โค๏ธ Hero's health: 50/100
hero.move();          // ๐Ÿšถ Hero limps slowly...
hero.takeDamage(30);  // ๐Ÿ’ฅ Hero takes 30 damage! Ouch! โค๏ธ Hero's health: 20/100
hero.attack();        // ๐Ÿ˜ฐ Hero is too weak to attack!
hero.heal();          // ๐Ÿ’‰ Hero uses emergency healing! โค๏ธ Hero's health: 60/100
hero.move();          // ๐Ÿšถ Hero limps slowly...

Async State Transitions ๐Ÿ”„

Real-world applications often need async operations:

// ๐Ÿ“ก Connection states with async operations
interface ConnectionState {
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  sendData(data: string): Promise<void>;
}

class NetworkConnection {
  private state: ConnectionState;
  
  constructor() {
    this.state = new DisconnectedState(this);
  }
  
  setState(state: ConnectionState): void {
    this.state = state;
  }
  
  // ๐ŸŽฏ Public async methods
  async connect(): Promise<void> {
    await this.state.connect();
  }
  
  async disconnect(): Promise<void> {
    await this.state.disconnect();
  }
  
  async sendData(data: string): Promise<void> {
    await this.state.sendData(data);
  }
}

// ๐Ÿ”Œ Disconnected state
class DisconnectedState implements ConnectionState {
  constructor(private connection: NetworkConnection) {}
  
  async connect(): Promise<void> {
    console.log("๐Ÿ”„ Connecting...");
    this.connection.setState(new ConnectingState(this.connection));
    
    // ๐ŸŽฒ Simulate connection attempt
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    const success = Math.random() > 0.3;
    if (success) {
      console.log("โœ… Connected successfully!");
      this.connection.setState(new ConnectedState(this.connection));
    } else {
      console.log("โŒ Connection failed!");
      this.connection.setState(new DisconnectedState(this.connection));
    }
  }
  
  async disconnect(): Promise<void> {
    console.log("๐Ÿ”Œ Already disconnected!");
  }
  
  async sendData(data: string): Promise<void> {
    console.log("โŒ Cannot send data - not connected!");
  }
}

// ๐Ÿ”„ Connecting state
class ConnectingState implements ConnectionState {
  constructor(private connection: NetworkConnection) {}
  
  async connect(): Promise<void> {
    console.log("โณ Already connecting... please wait!");
  }
  
  async disconnect(): Promise<void> {
    console.log("๐Ÿ›‘ Cancelling connection attempt!");
    this.connection.setState(new DisconnectedState(this.connection));
  }
  
  async sendData(data: string): Promise<void> {
    console.log("โณ Cannot send data - still connecting!");
  }
}

// โœ… Connected state
class ConnectedState implements ConnectionState {
  constructor(private connection: NetworkConnection) {}
  
  async connect(): Promise<void> {
    console.log("๐Ÿ“ก Already connected!");
  }
  
  async disconnect(): Promise<void> {
    console.log("๐Ÿ”Œ Disconnecting...");
    await new Promise(resolve => setTimeout(resolve, 500));
    console.log("๐Ÿ‘‹ Disconnected!");
    this.connection.setState(new DisconnectedState(this.connection));
  }
  
  async sendData(data: string): Promise<void> {
    console.log(`๐Ÿ“ค Sending: "${data}"`);
    await new Promise(resolve => setTimeout(resolve, 300));
    console.log("โœ… Data sent successfully!");
  }
}

// ๐ŸŽฎ Let's test async states!
const network = new NetworkConnection();
(async () => {
  await network.sendData("Hello");     // โŒ Cannot send data - not connected!
  await network.connect();             // ๐Ÿ”„ Connecting... โœ… Connected successfully!
  await network.sendData("Hello!");    // ๐Ÿ“ค Sending: "Hello!" โœ… Data sent successfully!
  await network.disconnect();          // ๐Ÿ”Œ Disconnecting... ๐Ÿ‘‹ Disconnected!
})();

โš ๏ธ Common Pitfalls and Solutions

โŒ Wrong: State logic in context

// โŒ BAD: Context knows too much about states
class BadPlayer {
  private state: string = "stopped";
  
  play(): void {
    if (this.state === "stopped") {
      console.log("Playing...");
      this.state = "playing";
    } else if (this.state === "paused") {
      console.log("Resuming...");
      this.state = "playing";
    } else {
      console.log("Already playing!");
    }
  }
  
  // ๐Ÿ˜ฑ This gets messy fast!
}

โœ… Correct: Encapsulated state behavior

// โœ… GOOD: States handle their own logic
class GoodPlayer {
  private state: PlayerState;
  
  constructor() {
    this.state = new StoppedState(this);
  }
  
  setState(state: PlayerState): void {
    this.state = state;
  }
  
  play(): void {
    this.state.play(); // ๐ŸŽฏ Delegate to state!
  }
}

โŒ Wrong: Forgetting state transitions

// โŒ BAD: State doesn't transition
class BrokenState implements PlayerState {
  play(): void {
    console.log("Playing!");
    // ๐Ÿ˜ฑ Forgot to change state!
  }
  
  pause(): void {
    console.log("Can't pause!");
  }
  
  stop(): void {
    console.log("Can't stop!");
  }
}

โœ… Correct: Proper state transitions

// โœ… GOOD: States transition properly
class WorkingState implements PlayerState {
  constructor(private player: MusicPlayer) {}
  
  play(): void {
    console.log("โ–ถ๏ธ Playing!");
    this.player.setState(new PlayingState(this.player)); // ๐ŸŽฏ Transition!
  }
  
  pause(): void {
    console.log("โš ๏ธ Can't pause from this state");
  }
  
  stop(): void {
    console.log("โน๏ธ Stopping!");
    this.player.setState(new StoppedState(this.player)); // ๐ŸŽฏ Transition!
  }
}

๐Ÿ› ๏ธ Best Practices

1. Keep States Focused ๐ŸŽฏ

Each state should have a single responsibility and clear transitions.

2. Use Type Safety ๐Ÿ’ช

Leverage TypeScriptโ€™s type system to ensure all states implement required methods.

3. Document State Transitions ๐Ÿ“

Create clear documentation or diagrams showing how states transition.

4. Consider State History ๐Ÿ“š

Sometimes you need to track previous states for undo/redo functionality.

5. Test State Transitions ๐Ÿงช

Write tests that verify all possible state transitions work correctly.

// ๐Ÿ† Best practice example
abstract class BaseState implements PlayerState {
  constructor(protected player: MusicPlayer) {}
  
  // ๐ŸŽฏ Default implementations
  play(): void {
    this.logInvalidAction("play");
  }
  
  pause(): void {
    this.logInvalidAction("pause");
  }
  
  stop(): void {
    this.logInvalidAction("stop");
  }
  
  protected logInvalidAction(action: string): void {
    console.log(`โš ๏ธ Cannot ${action} from ${this.constructor.name}`);
  }
  
  // ๐Ÿ”„ Helper for transitions
  protected transitionTo(StateClass: new (player: MusicPlayer) => PlayerState): void {
    this.player.setState(new StateClass(this.player));
  }
}

// ๐ŸŽต Now states are cleaner!
class BetterPlayingState extends BaseState {
  pause(): void {
    console.log("โธ๏ธ Pausing!");
    this.transitionTo(PausedState);
  }
  
  stop(): void {
    console.log("โน๏ธ Stopping!");
    this.transitionTo(StoppedState);
  }
}

๐Ÿงช Hands-On Exercise

Create a traffic light system with proper state management! ๐Ÿšฆ

Requirements:

  1. Three states: Red, Yellow, Green
  2. Proper transitions: Red โ†’ Green โ†’ Yellow โ†’ Red
  3. Timer-based automatic transitions
  4. Manual override for emergencies

Your challenge:

// ๐ŸŽฏ Your task: Implement a traffic light system!
interface TrafficLightState {
  // What methods should go here? ๐Ÿค”
}

class TrafficLight {
  // Implement the context! ๐Ÿ’ช
}

// Implement the states! ๐Ÿšฆ
๐Ÿ’ก Click here for the solution!
// ๐Ÿšฆ Traffic light implementation
interface TrafficLightState {
  enter(): void;
  exit(): void;
  next(): void;
  emergency(): void;
}

class TrafficLight {
  private state: TrafficLightState;
  private timer?: NodeJS.Timeout;
  
  constructor() {
    this.state = new RedLight(this);
    this.state.enter();
  }
  
  setState(state: TrafficLightState): void {
    this.state.exit();
    this.state = state;
    this.state.enter();
  }
  
  setTimer(callback: () => void, duration: number): void {
    this.clearTimer();
    this.timer = setTimeout(callback, duration);
  }
  
  clearTimer(): void {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = undefined;
    }
  }
  
  // ๐ŸŽฏ Public methods
  next(): void {
    this.state.next();
  }
  
  emergency(): void {
    this.state.emergency();
  }
}

// ๐Ÿ”ด Red light state
class RedLight implements TrafficLightState {
  constructor(private light: TrafficLight) {}
  
  enter(): void {
    console.log("๐Ÿ”ด RED - Stop! Cars must wait");
    this.light.setTimer(() => this.next(), 3000);
  }
  
  exit(): void {
    console.log("Leaving red state...");
  }
  
  next(): void {
    this.light.setState(new GreenLight(this.light));
  }
  
  emergency(): void {
    console.log("๐Ÿšจ Emergency! Staying RED for safety!");
    this.light.clearTimer();
  }
}

// ๐ŸŸข Green light state
class GreenLight implements TrafficLightState {
  constructor(private light: TrafficLight) {}
  
  enter(): void {
    console.log("๐ŸŸข GREEN - Go! Cars can proceed");
    this.light.setTimer(() => this.next(), 3000);
  }
  
  exit(): void {
    console.log("Leaving green state...");
  }
  
  next(): void {
    this.light.setState(new YellowLight(this.light));
  }
  
  emergency(): void {
    console.log("๐Ÿšจ Emergency! Switching to YELLOW!");
    this.light.setState(new YellowLight(this.light));
  }
}

// ๐ŸŸก Yellow light state
class YellowLight implements TrafficLightState {
  constructor(private light: TrafficLight) {}
  
  enter(): void {
    console.log("๐ŸŸก YELLOW - Caution! Prepare to stop");
    this.light.setTimer(() => this.next(), 1000);
  }
  
  exit(): void {
    console.log("Leaving yellow state...");
  }
  
  next(): void {
    this.light.setState(new RedLight(this.light));
  }
  
  emergency(): void {
    console.log("๐Ÿšจ Emergency! Going to RED!");
    this.light.setState(new RedLight(this.light));
  }
}

// ๐ŸŽฎ Test the traffic light!
const trafficLight = new TrafficLight();
// ๐Ÿ”ด RED - Stop! Cars must wait
// ... auto transitions happen with timers

// Manual control
trafficLight.next();      // Forces next state
trafficLight.emergency(); // Emergency override!

Great job! Youโ€™ve implemented a fully functional traffic light system! ๐ŸŽ‰

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered the State Pattern! Hereโ€™s what you learned:

  • ๐ŸŽฏ State Pattern encapsulates behavior based on object state
  • ๐Ÿ”„ States handle their own transitions keeping logic organized
  • ๐Ÿ’ช TypeScript interfaces ensure all states implement required methods
  • ๐ŸŽจ Clean separation of concerns makes code maintainable
  • ๐Ÿš€ Async states are possible for real-world applications

The State Pattern transforms complex conditional logic into elegant, maintainable code. No more massive switch statements or if-else chains! ๐ŸŽ‰

๐Ÿค Next Steps

Congratulations on mastering the State Pattern! ๐Ÿ† Youโ€™ve added a powerful tool to your TypeScript toolkit.

Ready for more patterns? Check out:

  • ๐Ÿ“˜ Strategy Pattern - Choose algorithms at runtime
  • ๐Ÿ“˜ Command Pattern - Encapsulate requests as objects
  • ๐Ÿ“˜ Observer Pattern - Subscribe to state changes

Keep practicing with real projects โ€“ maybe build a game character system or a workflow engine! The possibilities are endless! ๐Ÿ’ช

Remember: Great code isnโ€™t just about making it work; itโ€™s about making it elegant, maintainable, and fun to work with! Happy coding! ๐Ÿš€โœจ