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.
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 DETAILSWhat 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.