Welcome to the Behavioral Patterns course! In this lesson, we will explore the Command Pattern, a fundamental design pattern that is highly useful for promoting flexible and reusable code. This pattern is particularly effective in scenarios where you need to parameterize objects with operations, queues, or logs.
You might recall from previous lessons that behavioral design patterns assist with object communication and responsibility distribution within your software. The Command Pattern encapsulates a request as an object, thereby allowing users to parameterize clients with queues, requests, and operations. This encapsulation enables us to decouple the sender from the receiver, enhancing the flexibility and maintainability of the system.
The Command Pattern involves creating a command interface with an execute
method. We create concrete command classes that implement this interface, each representing a specific action. Finally, we integrate these commands with a request invoker to execute actions. This structure allows us to easily extend or modify commands without changing the invoker or the receiver.
To understand the Command Pattern, we should first identify its essential components: Command, Receiver, Concrete Commands, and Invoker. Here's a breakdown of each component, along with their implementations. Each of these components plays a critical role in decoupling the sender and receiver, thereby making the system more modular and flexible.
The Command
interface requires an execute
method that all concrete commands must implement. This setup provides a consistent method signature for executing various commands, making the system easier to extend. Since JavaScript doesn't have a formal interface or abstract class system, we can establish a convention using regular JavaScript classes.
JavaScript1class Command { 2 execute() { 3 throw new Error("Execute method must be implemented"); 4 } 5}
The receiver is the object that performs the actual action. In our example, the Light
class will serve as the receiver that can turn the light on or off. The receiver contains the logic that gets executed when the command is invoked.
JavaScript1class Light { 2 on() { 3 console.log("Light is on."); 4 } 5 6 off() { 7 console.log("Light is off."); 8 } 9}
Concrete command classes implement the Command
interface and are responsible for executing the receiver's methods. They encapsulate the receiver object and invoke the appropriate actions. In our example, we create LightOnCommand
and LightOffCommand
. These concrete commands translate user actions into calls to the receiver.
JavaScript1class LightOnCommand extends Command { 2 constructor(light) { 3 super(); 4 this.light = light; 5 } 6 7 execute() { 8 this.light.on(); 9 } 10}
JavaScript1class LightOffCommand extends Command { 2 constructor(light) { 3 super(); 4 this.light = light; 5 } 6 7 execute() { 8 this.light.off(); 9 } 10}
The invoker is the object that sends a request to execute a command. It holds a command object and can execute it. In our example, the RemoteControl
class is the invoker. The invoker is responsible for triggering the appropriate command based on user actions as it knows nothing about the actual operations that are performed.
JavaScript1class RemoteControl { 2 constructor() { 3 this.command = null; 4 } 5 6 setCommand(command) { 7 this.command = command; 8 } 9 10 pressButton() { 11 if (this.command) { 12 this.command.execute(); 13 } 14 } 15}
Now that we have broken down the Command Pattern into its core components, let's put it all together in a cohesive example. The RemoteControl
(invoker) holds and triggers the command objects without knowing their implementation details. The LightOnCommand
and LightOffCommand
(concrete commands) encapsulate actions on the Light
(receiver), translating invoker requests into specific receiver operations. This setup decouples the sender (invoker) from the receiver, promoting flexibility and extensibility in the system. This example demonstrates how encapsulating requests as objects can significantly simplify the design of a system with multiple actions and receivers.
JavaScript1class Command { 2 execute() { 3 throw new Error("Execute method must be implemented"); 4 } 5} 6 7class Light { 8 on() { 9 console.log("Light is on."); 10 } 11 12 off() { 13 console.log("Light is off."); 14 } 15} 16 17class LightOnCommand extends Command { 18 constructor(light) { 19 super(); 20 this.light = light; 21 } 22 23 execute() { 24 this.light.on(); 25 } 26} 27 28class LightOffCommand extends Command { 29 constructor(light) { 30 super(); 31 this.light = light; 32 } 33 34 execute() { 35 this.light.off(); 36 } 37} 38 39class RemoteControl { 40 constructor() { 41 this.command = null; 42 } 43 44 setCommand(command) { 45 this.command = command; 46 } 47 48 pressButton() { 49 if (this.command) { 50 this.command.execute(); 51 } 52 } 53} 54 55const light = new Light(); 56const lightOn = new LightOnCommand(light); 57const lightOff = new LightOffCommand(light); 58 59const remote = new RemoteControl(); 60remote.setCommand(lightOn); 61remote.pressButton(); // Output: Light is on. 62remote.setCommand(lightOff); 63remote.pressButton(); // Output: Light is off.
To make the example more dynamic and extendable while maintaining the proper usage of the Command Pattern with an invoker, we can introduce an object within the invoker to manage our command objects. This adjustment allows us to easily look up and execute commands based on keys while still preserving the invoker's role. Here’s an updated version of the main section:
JavaScript1class RemoteControl { 2 constructor() { 3 this.commands = {}; 4 } 5 6 setCommand(name, command) { 7 this.commands[name] = command; 8 } 9 10 pressButton(name) { 11 if (this.commands[name]) { 12 this.commands[name].execute(); 13 } else { 14 console.log(`No command named '${name}' found.`); 15 } 16 } 17} 18 19const light = new Light(); 20const lightOn = new LightOnCommand(light); 21const lightOff = new LightOffCommand(light); 22 23const remote = new RemoteControl(); 24remote.setCommand("lightOn", lightOn); 25remote.setCommand("lightOff", lightOff); 26 27// Simulate user input 28remote.pressButton("lightOn"); // Output: Light is on. 29remote.pressButton("lightOff"); // Output: Light is off.
In this enhancement, commands are stored in an object within the RemoteControl
(invoker), simplifying command referencing and execution via keys. This structure maintains the integrity of the Command Pattern, ensuring that the invoker triggers the commands. This dynamic structure helps handle various inputs and extends the system with ease.
Understanding and applying the Command Pattern is vital for writing maintainable and scalable code. This pattern allows you to decouple the sender of a request from its receiver, which can lead to more modular and easier-to-maintain systems. Consider a smart home system where various devices can be controlled via commands. By using the Command Pattern, you can seamlessly add new commands for different devices without altering existing code. This flexibility reduces the risk of bugs and simplifies code management, making your software more robust and easier to extend. Implementing this pattern can significantly improve the design and flexibility of your software architecture.