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 remember that behavioral design patterns help 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 then create concrete command classes that implement this interface, each representing a specific action. Finally, we'll integrate these commands with a request invoker to execute the 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:
Each of these components plays a critical role in decoupling the sender and receiver, thereby making the system more modular and flexible. Here's a breakdown of each component, along with their implementations.
The Command
interface declares an Execute
method that must be implemented by all concrete commands. This interface will help us define the actions to be executed. This interface enables a consistent method signature for executing various commands, making the system easier to extend.
C#1public interface ICommand 2{ 3 // Method signature for executing commands. 4 void Execute(); 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 actual logic that gets executed when the command is invoked.
C#1public class Light 2{ 3 // Turns the light on. 4 public void On() 5 { 6 Console.WriteLine("Light is on."); 7 } 8 9 // Turns the light off. 10 public void Off() 11 { 12 Console.WriteLine("Light is off."); 13 } 14}
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 the LightOnCommand
. This command acts as an intermediary, translating user actions into calls to the receiver.
C#1public class LightOnCommand : ICommand 2{ 3 // The light object that this command will manipulate. 4 private Light light; 5 6 // Constructor requires a light object to perform actions on. 7 public LightOnCommand(Light light) 8 { 9 this.light = light; 10 } 11 12 // Executes the On command of the light. 13 public void Execute() 14 { 15 light.On(); 16 } 17}
Another vital concrete command class in our example is the LightOffCommand
. Like LightOnCommand
, this class implements the Command
interface and is responsible for executing the receiver's methods. It encapsulates the receiver object and invokes the appropriate actions, thereby acting as an intermediary.
C#1public class LightOffCommand : ICommand 2{ 3 // The light object that this command will manipulate. 4 private Light light; 5 6 // Constructor requires a light object to perform actions on. 7 public LightOffCommand(Light light) 8 { 9 this.light = light; 10 } 11 12 // Executes the Off command of the light. 13 public void Execute() 14 { 15 light.Off(); 16 } 17}
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.
C#1public class RemoteControl 2{ 3 // The command to be executed by the remote control. 4 private ICommand? command; 5 6 // Sets the command to be executed. 7 public void SetCommand(ICommand command) 8 { 9 this.command = command; 10 } 11 12 // Executes the set command, if any. 13 public void PressButton() 14 { 15 if (command != null) 16 { 17 command.Execute(); 18 } 19 } 20}
Now that we have broken down the Command Pattern into its core components, let's put it all together in a cohesive example.
C#1class Program 2{ 3 static void Main() 4 { 5 // Receiver: the light that will be controlled. 6 Light light = new Light(); 7 8 // Concrete commands to turn the light on and off. 9 ICommand lightOn = new LightOnCommand(light); 10 ICommand lightOff = new LightOffCommand(light); 11 12 // Invoker: remote control to execute the commands. 13 RemoteControl remote = new RemoteControl(); 14 15 // Set the command to turn the light on and press the button. 16 remote.SetCommand(lightOn); 17 remote.PressButton(); 18 // Output: Light is on. 19 20 // Set the command to turn the light off and press the button. 21 remote.SetCommand(lightOff); 22 remote.PressButton(); 23 // Output: Light is off. 24 } 25}
In this example:
RemoteControl
(invoker) holds and triggers the command objects without knowing their implementation details.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. It demonstrates how encapsulating requests as objects can significantly simplify the design of a system with multiple actions and receivers.
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.