Introduction
In Week 06 Task 03, I continued learning Design Patterns in JavaScript.
For this task, I implemented the Command Pattern by building a simple text editor with support for editing operations and undo functionality.
The main goal was to understand how commands can encapsulate actions as objects and how this makes operations easier to manage, execute, and reverse.
I also wrote tests using Node.js's built-in assert module.
What is the Command Pattern?
The Command Pattern turns a request or action into a separate object.
Instead of directly calling a method to perform an operation, we create a command object that knows:
- What action should be performed
- How to perform it
- How to undo it
A simple structure looks like:
User
|
↓
Invoker
|
↓
Command
|
↓
Receiver
For this task:
EditorInvoker
|
↓
AppendCommand / DeleteCommand
|
↓
TextEditor
The TextEditor performs the actual work, while the command objects describe what operation should happen.
Components of My Implementation
I used four main components:
- TextEditor — Receiver
- AppendCommand — Command
- DeleteCommand — Command
- EditorInvoker — Invoker
1. TextEditor — Receiver
The receiver is the object that actually performs the operation.
In my implementation, TextEditor is the receiver.
It manages the text and provides operations such as:
- Append text
- Delete text
- Get current text
A simplified example:
class TextEditor {
constructor() {
this.text = "";
}
append(value) {
this.text += value;
}
delete(count) {
this.text = this.text.slice(0, -count);
}
getText() {
return this.text;
}
}
The important idea is that the receiver does the actual work.
2. AppendCommand
The AppendCommand represents the action of adding text.
It stores the text that needs to be appended and knows how to execute and undo that operation.
class AppendCommand {
constructor(editor, text) {
this.editor = editor;
this.text = text;
}
execute() {
this.editor.append(this.text);
}
undo() {
this.editor.delete(this.text.length);
}
}
When execute() is called, the text is added.
When undo() is called, the same text is removed.
For example:
Before:
Hello
Append " World"
After:
Hello World
Undo:
Hello
3. DeleteCommand
The DeleteCommand represents a delete operation.
It removes characters from the editor and stores enough information to restore them.
class DeleteCommand {
constructor(editor, count) {
this.editor = editor;
this.count = count;
this.deletedText = "";
}
execute() {
this.deletedText =
this.editor.getText().slice(-this.count);
this.editor.delete(this.count);
}
undo() {
this.editor.append(this.deletedText);
}
}
The important part is that the command remembers what was deleted.
That information is needed when the command is undone.
4. EditorInvoker
The invoker is responsible for executing commands and keeping track of command history.
class EditorInvoker {
constructor() {
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
The invoker doesn't need to know how each command works.
It simply tells the command:
command.execute();
or:
command.undo();
This keeps the invoker separate from the actual editing logic.
How the Complete Flow Works
Suppose we want to append "Hello".
First, we create the command:
const command =
new AppendCommand(editor, "Hello");
Then the invoker executes it:
invoker.execute(command);
The flow is:
invoker.execute()
|
↓
AppendCommand.execute()
|
↓
TextEditor.append()
|
↓
"Hello"
The command is then stored in the history.
Undo Flow
When we call:
invoker.undo();
the most recent command is removed from history and its undo() method is called.
invoker.undo()
|
↓
Get last command
|
↓
command.undo()
|
↓
Reverse previous operation
This provides a simple undo system.
Why Keep Command History?
Command history is important because it allows us to reverse operations in reverse order.
For example:
1. Append "Hello"
2. Append " World"
3. Delete 6 characters
History:
[Append, Append, Delete]
If we call undo:
Delete → undone
Then:
Append " World" → undone
Then:
Append "Hello" → undone
This is called Last-In, First-Out (LIFO) behavior.
The most recent command is undone first.
Example
A simple usage could look like:
const editor = new TextEditor();
const invoker = new EditorInvoker();
invoker.execute(
new AppendCommand(editor, "Hello")
);
invoker.execute(
new AppendCommand(editor, " World")
);
console.log(editor.getText());
Output:
Hello World
Now undo the last operation:
invoker.undo();
console.log(editor.getText());
Output:
Hello
Testing
I used Node.js's built-in assert module to test the Command Pattern.
The tests covered:
- Appending text
- Deleting text
- Undoing append operations
- Undoing delete operations
- Multiple commands
- Undoing commands in the correct order
- Calling undo when there is no command history
Example:
assert.strictEqual(
editor.getText(),
"Hello World"
);
invoker.undo();
assert.strictEqual(
editor.getText(),
"Hello"
);
These tests helped verify that commands were being executed and reversed correctly.
Project Structure
The final Task 03 structure was:
week-06/
└── Task-03/
├── README.md
├── text-editor.js
└── test.js
Each file had a specific responsibility:
| File | Purpose |
|---|---|
text-editor.js |
Command Pattern implementation |
test.js |
Tests |
README.md |
Documentation |
Real-World Examples
The Command Pattern is commonly useful when actions need to be stored, queued, logged, or undone.
Examples include:
Text Editors
Type → Delete → Undo → Redo
GUI Applications
Click → Execute Command
Transaction Systems
Create → Update → Delete
Task Queues
Command 1
Command 2
Command 3
Commands can be executed later by a worker.
Benefits of the Command Pattern
The Command Pattern provides several advantages:
1. Separation of Responsibilities
The receiver performs the actual work, while commands represent actions.
2. Undo Support
Commands can store the information required to reverse operations.
3. History
Executed commands can be stored and managed.
4. Extensibility
New commands can be added without heavily modifying existing code.
5. Queuing
Commands can be stored and executed later.
What I Learned
This task helped me understand how an operation can be represented as an object.
The most important idea I learned is:
Action → Command Object → Execute
→ Undo
Instead of directly modifying the editor, I can create commands that describe what should happen.
I also learned how command history can be used to implement undo functionality.
Key Takeaways
The main concepts I learned from this task are:
Command → Encapsulates an action
Receiver → Performs the actual operation
Invoker → Executes and manages commands
History → Stores executed commands
Undo → Reverses the most recent command
The Command Pattern is especially useful when operations need to be undoable, queued, logged, or controlled independently.
Conclusion
Week 06 Task 03 gave me practical experience with the Command Pattern in JavaScript.
I built a simple text editor where editing operations were represented as command objects. I also implemented command history and undo functionality.
This task helped me understand how separating the command, invoker, and receiver can make an application more modular and easier to extend.
Overall, Week 06 helped me move beyond simply writing JavaScript code and understand how design patterns can be used to structure real applications.
Top comments (2)
Solid write-up, the DeleteCommand remembering what it deleted before undoing is the detail that actually makes undo work instead of just calling it. Natural next step once this clicks is a redo stack alongside the undo one, same pattern, just two histories instead of one.
Thank you for the comment this means a lot 👋🤍