Skip to main content

Real World Implementation Of The Command Design Pattern

Consider that you have a file system where you can create, read, update, and delete (CRUD operations) files. Each operation can be encapsulated in a command, and a single FileSystem class can execute these commands.

Real World Implementation

Here's how you might implement this in TypeScript:

interface ICommand {
execute(): void;
undo(): void;
}

class MyFileSystem {
private commandQueue: ICommand[] = [];

public addCommand(command: ICommand) {
this.commandQueue.push(command);
}

public executeCommand() {
if (this.commandQueue.length > 0) {
let command = this.commandQueue.shift();
command?.execute();
}
}

public undoCommand() {
if (this.commandQueue.length > 0) {
let command = this.commandQueue.pop();
command?.undo();
}
}

hasCommands(): boolean {
return this.commandQueue.length > 0;
}
}

class CreateFileCommand implements ICommand {
private path: string;

constructor(path: string) {
this.path = path;
}

execute() {
console.log(`Creating file at ${this.path}`);
// Here would be logic for creating a file at the given path
}

undo() {
console.log(`Deleting file at ${this.path}`);
// Here would be logic for deleting a file at the given path
}
}

class DeleteFileCommand implements ICommand {
private path: string;

constructor(path: string) {
this.path = path;
}

execute() {
console.log(`Deleting file at ${this.path}`);
// Here would be logic for deleting a file at the given path
}

undo() {
console.log(`Restoring file at ${this.path}`);
// Here would be logic for restoring a file at the given path
}
}

class UpdateFileCommand implements ICommand {
private path: string;
private newContent: string;
private oldContent: string;

constructor(path: string, newContent: string, oldContent: string) {
this.path = path;
this.newContent = newContent;
this.oldContent = oldContent;
}

execute() {
console.log(
`Updating file at ${this.path} with new content: ${this.newContent}`
);
// Here would be logic for updating a file at the given path with the new content
}

undo() {
console.log(
`Reverting file at ${this.path} to old content: ${this.oldContent}`
);
// Here would be logic for reverting the file back to the old content
}
}

class ReadFileCommand implements ICommand {
private path: string;

constructor(path: string) {
this.path = path;
}

execute() {
console.log(`Reading file at ${this.path}`);
// Here would be logic for reading a file at the given path
}

undo() {
console.log(
`Undo operation not available for reading file at ${this.path}`
);
// Reading a file doesn't change its state, so there's nothing to undo
}
}

let myFileSystem = new MyFileSystem();

// Creating a file
let createFileCommand = new CreateFileCommand("/path/to/myFile.txt");
myFileSystem.addCommand(createFileCommand);

// Updating the file
let updateFileCommand = new UpdateFileCommand(
"/path/to/myFile.txt",
"new content",
"old content"
);
myFileSystem.addCommand(updateFileCommand);

// Reading the file
let readFileCommand = new ReadFileCommand("/path/to/myFile.txt");
myFileSystem.addCommand(readFileCommand);

// Deleting the file
let deleteFileCommand = new DeleteFileCommand("/path/to/myFile.txt");
myFileSystem.addCommand(deleteFileCommand);

// Executing commands in the queue
while (myFileSystem.hasCommands()) {
// assuming `hasCommands` method checks if the queue has commands
myFileSystem.executeCommand();
}

// Undoing the last command
myFileSystem.undoCommand(); // Undoes the delete command, effectively restoring the file

In the real-world, of course, the execute methods of the command classes would contain the actual logic for performing the respective file operations, rather than just logging a message. But this simplified example gives you an idea of how you might use the Command pattern to handle different types of operations on a file system.

Advantages Of The Command Pattern

The Command Pattern offers several advantages, especially in complex applications

Decoupling

The Command Pattern decouples the object that invokes an operation (the invoker) from the one that knows how to perform it (the receiver). The invoker doesn't need to know any specifics; it just needs to know how to issue a command.

let fileSystem = new FileSystem();

// Creating a file
let createFileCommand = new CreateFileCommand("/path/to/myFile.txt");
fileSystem.executeCommand(createFileCommand);

In this code snippet, FileSystem (the invoker) doesn't know anything about how to create a file. It simply delegates this to the CreateFileCommand ( the command object).

Extension

New commands can be added without changing existing code, which makes the system more extensible.

class NewCommand implements ICommand {
private someDependency: any;

constructor(someDependency: any) {
this.someDependency = someDependency;
}

execute() {
console.log(`Executing NewCommand with ${this.someDependency}`);
// Logic for the new command goes here
}
}

You can add a NewCommand class that implements ICommand, and it can be executed by the existing FileSystem class without any changes to FileSystem.

Complex Commands

Complex commands, which involve many steps or triggering several operations, can be encapsulated in a command object. This makes the code more readable and manageable.

class ComplexCommand implements ICommand {
private command1: ICommand;
private command2: ICommand;

constructor(command1: ICommand, command2: ICommand) {
this.command1 = command1;
this.command2 = command2;
}

execute() {
this.command1.execute();
this.command2.execute();
}
}

In the ComplexCommand class, multiple commands can be executed in the execute method, simplifying the invoker's usage.

Undo/Redo operations

By maintaining a history of commands, you can easily implement undo and redo operations. This feature isn't demonstrated in the given code, but the concept would involve storing a list of executed command objects and invoking an undo method on the command object to reverse its operation.

Deferred and asynchronous operations

The Command Pattern allows operations to be executed at different times or by different threads. The invoker can execute commands without waiting for them to complete, which can be helpful in multi-threaded or asynchronous programming.

Please note that not all these advantages are explicitly demonstrated in the provided code, but they give you an idea of the potential benefits of the Command pattern.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

Time To Transition From JavaScript To TypeScript

Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.

SEE COURSE DETAILS

What Can You Do Next 🙏😊

If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for software developers.

YouTube @cloudaffle