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:
CommandInterface: Define an interface namedCommandwith a single method:execute(): void.- Concrete Commands: Create at least two distinct concrete command classes that implement the
Commandinterface. These commands should perform different actions. InvokerClass: Create anInvokerclass responsible for holding and executing commands. TheInvokershould have a method to set a command and a method to trigger its execution.- Undoable Commands: Introduce a mechanism for commands that can be undone. This could involve extending the
Commandinterface or creating a new one (e.g.,UndoableCommand) that includes anundo(): voidmethod. - Concrete Undoable Command: Create at least one concrete command class that implements the undo functionality.
CommandHistory(Optional but Recommended): Consider creating aCommandHistoryclass that can store a sequence of executed commands and potentially facilitate undoing multiple operations.
Expected Behavior:
- The
Invokershould be able to execute anyCommand. - The
Invokershould be able to executeUndoableCommands and trigger theirundomethods. - When an
UndoableCommandis executed and then undone, its effect should be reversed.
Edge Cases to Consider:
- What happens if the
Invokertries to execute a command when none is set? - What happens if an
UndoableCommandis 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
UndoableCommandinterface. The baseCommandinterface might not have anundomethod. - Think about the lifecycle of a command and how its state might be managed, especially for undoable operations.
- The
Invokermight need to manage a collection of commands if it's intended to execute a sequence of operations. TheCommandHistoryclass can be a good abstraction for this. - For undoable commands, ensure the
undomethod correctly reverses the action of theexecutemethod.