Command Pattern in TypeScript — ✨ Structural Design Pattern #2

365kim
3 min readJul 22, 2023

--

Photo by Daniel Eledut on Unsplash

🙋🏻‍♀️ What is the Command Pattern?

The Command Pattern is a behavioral design pattern that provides a way to encapsulate a request as a stand-alone object with all information about the request, promoting decoupling between objects.

In a sense, the Command Pattern is a means of encapsulating a method call, an operation, or a trigger event as an object, called as command. Thus, you can create a list of actions or tasks that your program will perform later.

  • command: an object with execute()method, which contains all required information to execute the method.
  • receiver: an object that knows how to perform the operations associated with carrying out a request. When a command is executed, the corresponding operation(s) in the receiver is called.
  • invoker: an object that sends the command to the receiver not aware of the specific concrete command it’s invoking, which promotes loose coupling in the system.

✨ Simple TypeScript Example

Let’s start from the Command interface for all commands. It mandates a single method, execute(), which every concrete command class will implement.

interface Command {
execute(): void
}

The receiver has the actual implementations of the operations to be performed.

class Receiver {
turnOn(): void {
console.log('operations to turn on')
}

turnOff(): void {
console.log('operations to turn off')
}
}

Now you need an invoker class that stores the command objects and calls their execute() method when required, and also keeps track of command history for usability.

class Invoker {
commands: { [id: string]: Command }
history: { id: string; timeStamp: number }[]

constructor() {
this.commands = {}
this.history = []
}

register(commandId: string, command: Command): void {
this.commands[commandId] = command
}

execute(commandId: string): void {
const command = this.commands[commandId]

if (command === undefined) {
return console.log(`Command ${commandId} not found`)
}

command.execute()
this.history.push({ id: commandId, timeStamp: Date.now() })
}

redo(): void {
const lastCommandId = this.history[this.history.length - 1].id

this.execute(lastCommandId)
}
}

These are the concrete implementations of the Command interface, encapsulating an operation and its parameters.

class OnCommand implements Command {
receiver: Receiver

constructor(receiver: Receiver) {
this.receiver = receiver
}

execute(): void {
this.receiver.turnOn()
}
}

class OffCommand implements Command {
receiver: Receiver

constructor(receiver: Receiver) {
this.receiver = receiver
}

execute(): void {
this.receiver.turnOff()
}
}

In the client code, commands are registered with the invoker

const invoker = new Invoker()
const receiver = new Receiver()

invoker.register('TURN_ON', new OnCommand(receiver))
invoker.register('TURN_OFF', new OffCommand(receiver))

Then the invoker executes these encapsulated commands.

invoker.execute('TURN_ON')
invoker.execute('TURN_OFF')
invoker.execute('TURN_ON')
invoker.execute('TURN_OFF')
invoker.redo()

🧑‍💻 Use it or Avoid it

When to use it

The Command Pattern is useful in the following scenarios:

  1. Undo/Redo functionality: By encapsulating operations as commands, it becomes easier to implement undo/redo functionality, as each command can store the necessary information to reverse its execution.
  2. Queueing and scheduling requests: Commands can be queued or scheduled for execution at specific times or in a specific order.
  3. Callbacks and event handling: Commands can be used to encapsulate callbacks or event handlers, allowing you to pass around and execute these functions as objects.
  4. Decoupling request senders from receivers: The Command Pattern decouples the object that invokes the operation from the object that performs the operation. This allows for more flexibility and extensibility, as different commands can be dynamically bound to the sender.

When to avoid it

  • If you use it for trivial operations, it might add unnecessary complexity and result in a bloated codebase.
  • If you’re using it in a simple application where simple method calls would be more straightforward, the overhead of maintaining Command classes can outweigh the benefits

--

--

365kim
365kim

Written by 365kim

Web Front-End Developer who believes Every Day Counts!

No responses yet