Hone logo
Hone
Problems

Implementing the Command Pattern in TypeScript

This challenge focuses on implementing the Command design pattern in TypeScript. The Command pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This allows you to parameterize methods with different requests, delay or queue a request's execution, and support undoable operations.

Problem Description

Your task is to create a robust and flexible command system in TypeScript. You will need to define the core interfaces and classes that constitute the Command pattern. This system should be capable of handling various types of commands, including those that can be undone.

Key Requirements:

  1. Command Interface: Define an interface named Command with a single method: execute(): void.
  2. Concrete Commands: Create at least two distinct concrete command classes that implement the Command interface. These commands should perform different actions.
  3. Invoker Class: Create an Invoker class responsible for holding and executing commands. The Invoker should have a method to set a command and a method to trigger its execution.
  4. Undoable Commands: Introduce a mechanism for commands that can be undone. This could involve extending the Command interface or creating a new one (e.g., UndoableCommand) that includes an undo(): void method.
  5. Concrete Undoable Command: Create at least one concrete command class that implements the undo functionality.
  6. CommandHistory (Optional but Recommended): Consider creating a CommandHistory class that can store a sequence of executed commands and potentially facilitate undoing multiple operations.

Expected Behavior:

  • The Invoker should be able to execute any Command.
  • The Invoker should be able to execute UndoableCommands and trigger their undo methods.
  • When an UndoableCommand is executed and then undone, its effect should be reversed.

Edge Cases to Consider:

  • What happens if the Invoker tries to execute a command when none is set?
  • What happens if an UndoableCommand is undone when it hasn't been executed or has already been undone?

Examples

Example 1: Basic Command Execution

// Assume appropriate Command and Invoker classes are defined elsewhere

// Receiver for our commands
class Light {
    isOn: boolean = false;
    turnOn(): void {
        console.log("Light is ON");
        this.isOn = true;
    }
    turnOff(): void {
        console.log("Light is OFF");
        this.isOn = false;
    }
}

// Concrete Command: Turn Light On
class LightOnCommand implements Command {
    private light: Light;
    constructor(light: Light) {
        this.light = light;
    }
    execute(): void {
        this.light.turnOn();
    }
}

// Invoker
class Invoker {
    private command: Command | null = null;

    setCommand(command: Command): void {
        this.command = command;
    }

    executeCommand(): void {
        if (this.command) {
            this.command.execute();
        } else {
            console.log("No command set to execute.");
        }
    }
}

// --- Usage ---
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const invoker = new Invoker();

invoker.setCommand(lightOnCommand);
invoker.executeCommand();
// Expected Output:
// Light is ON

Example 2: Undoable Command

// Assume Light and Invoker classes from Example 1 are available.
// Also assume a Command interface.

// Undoable Command Interface
interface UndoableCommand extends Command {
    undo(): void;
}

// Concrete Undoable Command: Turn Light Off
class LightOffCommand implements UndoableCommand {
    private light: Light;
    constructor(light: Light) {
        this.light = light;
    }
    execute(): void {
        console.log("Executing LightOffCommand");
        this.light.turnOff();
    }
    undo(): void {
        console.log("Undoing LightOffCommand");
        this.light.turnOn(); // Reversing the action
    }
}

// Modified Invoker to handle undoable commands
class UndoableInvoker extends Invoker {
    private history: UndoableCommand[] = [];

    executeCommand(): void {
        if (this.command) {
            this.command.execute();
            if (this.command instanceof UndoableCommand) {
                this.history.push(this.command);
            }
        } else {
            console.log("No command set to execute.");
        }
    }

    undoLastCommand(): void {
        const lastCommand = this.history.pop();
        if (lastCommand) {
            lastCommand.undo();
        } else {
            console.log("No commands to undo.");
        }
    }
}

// --- Usage ---
const light = new Light(); // Assume Light class is defined
const lightOffCommand = new LightOffCommand(light);
const undoableInvoker = new UndoableInvoker();

undoableInvoker.setCommand(lightOffCommand);
undoableInvoker.executeCommand(); // Light is OFF

console.log("Current light state:", light.isOn); // Expected: false

undoableInvoker.undoLastCommand(); // Undoing LightOffCommand
// Expected Output:
// Executing LightOffCommand
// Light is OFF
// Current light state: false
// Undoing LightOffCommand
// Light is ON

console.log("Current light state after undo:", light.isOn); // Expected: true

Example 3: Handling No Command

// Assume Invoker class from Example 1

// --- Usage ---
const invoker = new Invoker();
invoker.executeCommand();
// Expected Output:
// No command set to execute.

Constraints

  • All type definitions (interfaces and classes) must be in TypeScript.
  • The solution should be well-organized, with clear separation of concerns.
  • Focus on demonstrating the core principles of the Command pattern.
  • The solution should be easily extensible to add new types of commands.
  • No external libraries are permitted for implementing the core design pattern logic.

Notes

  • Consider how to handle commands that are not undoable if you introduce an UndoableCommand interface. The base Command interface might not have an undo method.
  • Think about the lifecycle of a command and how its state might be managed, especially for undoable operations.
  • The Invoker might need to manage a collection of commands if it's intended to execute a sequence of operations. The CommandHistory class can be a good abstraction for this.
  • For undoable commands, ensure the undo method correctly reverses the action of the execute method.
Loading editor...
typescript