DEV Community

koushikmaya
koushikmaya

Posted on

# Week 06 Task 03: Command Pattern in JavaScript

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
Enter fullscreen mode Exit fullscreen mode

For this task:

EditorInvoker
      |
      ↓
AppendCommand / DeleteCommand
      |
      ↓
TextEditor
Enter fullscreen mode Exit fullscreen mode

The TextEditor performs the actual work, while the command objects describe what operation should happen.


Components of My Implementation

I used four main components:

  1. TextEditor — Receiver
  2. AppendCommand — Command
  3. DeleteCommand — Command
  4. 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The invoker doesn't need to know how each command works.

It simply tells the command:

command.execute();
Enter fullscreen mode Exit fullscreen mode

or:

command.undo();
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Then the invoker executes it:

invoker.execute(command);
Enter fullscreen mode Exit fullscreen mode

The flow is:

invoker.execute()
       |
       ↓
AppendCommand.execute()
       |
       ↓
TextEditor.append()
       |
       ↓
"Hello"
Enter fullscreen mode Exit fullscreen mode

The command is then stored in the history.


Undo Flow

When we call:

invoker.undo();
Enter fullscreen mode Exit fullscreen mode

the most recent command is removed from history and its undo() method is called.

invoker.undo()
      |
      ↓
Get last command
      |
      ↓
command.undo()
      |
      ↓
Reverse previous operation
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

History:

[Append, Append, Delete]
Enter fullscreen mode Exit fullscreen mode

If we call undo:

Delete → undone
Enter fullscreen mode Exit fullscreen mode

Then:

Append " World" → undone
Enter fullscreen mode Exit fullscreen mode

Then:

Append "Hello" → undone
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

Output:

Hello World
Enter fullscreen mode Exit fullscreen mode

Now undo the last operation:

invoker.undo();

console.log(editor.getText());
Enter fullscreen mode Exit fullscreen mode

Output:

Hello
Enter fullscreen mode Exit fullscreen mode

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"
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

GUI Applications

Click → Execute Command
Enter fullscreen mode Exit fullscreen mode

Transaction Systems

Create → Update → Delete
Enter fullscreen mode Exit fullscreen mode

Task Queues

Command 1
Command 2
Command 3
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
respect17 profile image
Kudzai Murimi •

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.

Collapse
 
koushikmaya profile image
koushikmaya •

Thank you for the comment this means a lot 👋🤍