At its core, the command pattern is a behavioral software design pattern that helps separate the logic of a request's execution from its actual implementation. This separation helps to promote greater flexibility and code organization in larger codebases. In this article, I'll go over the command pattern in C# so we can see it in action!
Maintaining well-organized code is crucial for any software engineer, and understanding design patterns -- such as the command pattern -- is an essential part of this process. Together we'll implement the command pattern in C# and you can see if this pattern will help in your current codebase.
Let's dive in!
What's In This Article: The Command Pattern in C#
Remember to follow along on these platforms:
// FIXME: social media icons coming back soon!
Understanding the Command Pattern
The Command Pattern is a behavioral design pattern that decouples the sender of a request from the object that performs the action requested. This software pattern encloses a request as an object, thereby allowing you to parameterize clients with different requests. The object that encapsulates the command may know how to invoke the action, but it doesn't necessarily know the receiver of the request.
In C#, the Command Pattern is used to encapsulate requests as objects, which is particularly helpful in constructing composite commands or transaction processing. The pattern is designed to enable the separation of application-specific functionality from the underlying system, ensuring that changes made to one do not affect the other.
By separating commands from the classes that use them, the command pattern allows you to create a more cohesive and maintainable codebase over time. This separation enables the code to scale and evolve over time without much impact on the system's overall architecture.
Command Pattern Example in C#
Let's say we have a fictional restaurant application where a customer can order a beverage. We would use the Command Pattern to encapsulate the request for the beverage order and its processing. The customer would create a concrete command object, specifying a drink order. The Invoker object, the waiter, sends the Concrete Command object to the receiver, the bartender, which then executes the request.
This separation of roles enables the code to scale and evolve over time while maintaining code modularity and cohesion -- At least, that's the theory behind following a pattern like this. In practice, we may find that the levels of abstraction and separation over-complicate, so it's important to remain pragmatic and choose what works situationally.
Here's an example of implementing the Command Pattern in C#:
// Command
public interface ICommand
{
void Execute();
}
// Concrete Command
public class BeverageOrderCommand : ICommand
{
private readonly Beverage _beverage;
private readonly Bartender _bartender;
public BeverageOrderCommand(Beverage beverage, Bartender bartender)
{
_beverage = beverage;
_bartender = bartender;
}
public void Execute()
{
_bartender.ExecuteDrinkOrder(_beverage);
}
}
// Receiver
public class Bartender
{
public void ExecuteDrinkOrder(Beverage beverage)
{
Console.WriteLine($"Preparing {beverage.Name}");
}
}
// Invoker
public class Waiter
{
private readonly ICommand _command;
public Waiter(ICommand command)
{
_command = command;
}
public void OrderDrink()
{
_command.Execute();
}
}
We'll revisit how to put this all together in a later section, so keep on reading!
Design Principles of the Command Pattern
The Command Pattern follows several principles of object-oriented design. Two of the most relevant principles are the Open-Closed principle and the Single Responsibility principle.
Open-Closed Principle
The Open-Closed principle states that software entities should be open for extension but closed for modification. In other words, once a class is defined and implemented, it should not be changed, except for maintenance purposes. Instead, new features should be added by creating new classes that inherit from or interface with the existing class.
When applied to the Command Pattern, the Open-Closed principle emphasizes the importance of creating concrete commands that can be extended over time, without modifying the existing code. This principle enables us to add new functionality to a command without changing the commands' existing code.
Single Responsibility Principle
The Single Responsibility principle states that every software entity should have only one responsibility. This means that a class should be designed in such a way that it only has one reason to change. In the context of the Command Pattern, the Command objects should have only one task to accomplish.
This can be helpful to recognize because if you have a module of your code that's doing "too much" (i.e. not a single responsibility), you may be able to ask if some can be factored into a pattern like the Command Pattern. Functionally, it should remain the same -- But the implementation and design may end up lending itself well to better organization of code.
SOLID Principle
The Command Pattern adheres to the SOLID principle, which is an acronym for five object-oriented design principles used in software development. Each letter represents a principle, as follows:
- S - Single Responsibility Principle: Each software entity should have only one responsibility. The Command Pattern follows this principle by creating commands that have only one task to accomplish.
- O - Open-Closed Principle: Software entities should be open for extension but closed for modification. The Command Pattern facilitates this principle through the use of abstract commands that define the behavior of the commands, and concrete classes that implement those behaviors.
- L - Liskov Substitution Principle: Subtypes should be substitutable for their base types. The Command Pattern facilitates this principle by ensuring that all commands implement the ICommand interface, making them substitutable.
- I - Interface Segregation Principle: Clients should only be exposed to relevant interfaces. The Command Pattern follows this principle by allowing clients to interact with the Invoker, which calls the commands, rather than with the commands themselves.
- D - Dependency Inversion Principle: Depend on abstractions, not on concretions. The Command Pattern follows this principle by allowing the Invoker to call commands using an ICommand interface, rather than a concrete command. This way, the Invoker does not depend on a specific command implementation.
Key Components of the Command Pattern
The Command Pattern is composed of four primary components:
- Invoker
- Command
- Concrete Command
- Receiver.
Each component plays a critical role in encapsulating requests as objects in the Command Pattern. As we explore each of these, we can consider how the example we looked at relates.
Invoker
The Invoker is responsible for invoking the commands and initiating their execution. It does not know the details of how the command is executed, unaware of the command's receiver. The Invoker only works with the Command object's generic interface, which is ICommand in C#. When the Invoker receives a command, it stores the command in a history of all invoked commands, providing support for undo and redo commands.
An example of an Invoker implementation in our C# example would be a Waiter class that executes a command by calling its execute() method. If we look at the implementation, we can see that the calls made inside of the Waiter don't suggest any shared knowledge of the implementation of the commands.
Command
The Command is the abstract encapsulation of a request, which includes the receiver object associated with this request. Commands expose an interface for invoking requests without coupling the sender's implementation to the requester's implementation. In other words, Command objects do not need to know anything about the object they report to - they merely execute the command's logic and call upon the receiver object when necessary.
In our C# example, the Command object implements the ICommand interface, which serves as the basis for all commands. You could use a base class or an abstract class, but our example just uses the interface.
Concrete Command
A Concrete Command extends Command and implements the execute() method in a way that defines a specific request and its receiver. Every concrete command corresponds to a specific operation on the receiver. This implementation is where the actual work is done. In other words, the concrete command is responsible for calling the appropriate receiver method(s) based on the request (the command).
An example of a Concrete Command implementation in our C# example is a BeverageOrderCommand, which implements the Execute() method to prepare the beverage.
Receiver
The Receiver is responsible for providing the necessary actions for carrying out the request. These actions occur as a result of the Concrete Command call to the receiver's execute() method. The receiver itself does not know anything about the command pattern – it only knows how to perform the action associated with the request that it received.
In our C# example, a Receiver implementation could be the class that defines the actual task to be executed, such as the Bartender class executing the beverage command.
Implementing the Command Pattern in C#: A Practical Guide
Implementing the Command Pattern in C# involves a few fundamental steps, but when done correctly, it can lead to better organization and maintainability of your code. Here is a step-by-step guide to implementing the Command Pattern in C# along with code examples to help you understand how it all works.
Step 1: Define the Command Interface
Define the ICommand interface. This interface defines the operation that needs to be executed when the command is executed.
public interface ICommand
{
void Execute();
}
Step 2: Design the Concrete Command Classes
Create concrete classes that implement the ICommand interface. In this concrete command class, specific parameters and methods are implemented.
public class BeverageOrderCommand : ICommand
{
private readonly Beverage _beverage;
private readonly Bartender _bartender;
public BeverageOrderCommand(Beverage beverage, Bartender bartender)
{
_beverage = beverage;
_bartender = bartender;
}
public void Execute()
{
_bartender.ExecuteDrinkOrder(_beverage);
}
}
Step 3: Implement the Invoker Class
The Invoker class stores the ICommand object and calls its specific Execute() method to run the command. This class receives a command object to run in the form of ICommand.
public class Waiter
{
private readonly ICommand _command;
public Waiter(ICommand command)
{
_command = command;
}
public void OrderDrink()
{
_command.Execute();
}
}
Step 4: Define the Receiver Class
This class holds the actual logic for performing the task. It receives the command sent from the client to execute an operation.
public class Bartender
{
public void ExecuteDrinkOrder(Beverage beverage)
{
Console.WriteLine($"Preparing {beverage.Name}");
}
}
Step 5: Create the Client
The client creates the command objects and passes them to the Invoker class to execute the command.
class Program
{
static void Main(string[] args)
{
var bartender = new Bartender();
ICommand order = new BeverageOrderCommand(new Beverage("Margarita"), bartender);
Waiter waiter = new(order); waiter.OrderDrink();
}
}
By following these steps, you can effectively implement the Command Pattern in C#. The Command Pattern leads to cleaner and more modular code, which can help maintain the system's structure over time.
Benefits and Drawbacks of the Command Pattern
The Command Pattern offers numerous benefits for software developers. By encapsulating requests as objects, you can effectively coordinate work on complex, multi-step processes. The pattern provides a uniform and consistent way to represent, store, and transmit requests. This helps to improve the overall structure and maintainability of your codebase.
Benefits of the Command Pattern in C#:
Below are some benefits of the Command Pattern:
- Improved code organization: The Command Pattern promotes improved code organization by separating the request from the objects that perform the action, making it easier to understand and modify code.
- Maintainability: Separating the requests into a Command object allows for better scalability and maintainability of code. Programs can be easily modified and updated without worrying about adverse side effects.
Reusability: With the Command Pattern, implementations of individual commands can be reused across the application.
Drawbacks of the Command Pattern in C#
Although the Command Pattern offers plenty of benefits, it also has a few drawbacks:
- Increased memory usage: Using the Command Pattern requires creating many small objects that require memory allocation. This overhead can increase the program's memory usage and performance latency, especially in high-performance applications.
- Increased complexity: Integrating the Command Pattern into an application can add complexity due to the need for additional classes and interfaces. This is a pretty common trade-off when implementing many design patterns or levels of abstraction.
- Possible overuse: Overusing the Command Pattern can lead to code redundancy and a convoluted application structure that makes it challenging to maintain, update, and debug.
A common misconception about the Command Pattern is that its use is limited to the undo/redo feature. While this is true, the Command Pattern offers much more than just undo and redo functionality. It promotes code maintainability, extensibility, and scalability, making it an essential part of any software developer's toolkit.
By understanding the benefits and drawbacks of the Command Pattern, you can determine if it is the best pattern for your application. Like with any software development pattern, the Command Pattern has its advantages and disadvantages, so it's important to know when and where it makes sense to use it.
Wrapping Up the Command Pattern in C#
The Command Pattern is a powerful and flexible design pattern that can assist software engineers in separating code execution from code triggering, increasing modularity and reusability. Applying the Command Pattern to C# code can help to organize large codebases, reduce complexity, promote high cohesion, and minimize coupling between objects. While there are some drawbacks to the pattern, the benefits far outweigh them in most cases.
Remember, the key components of the Command Pattern are the invoker, command, concrete command, and receiver. By following the SOLID principles, engineers can create clean and easy-to-maintain code. The challenge is to identify an appropriate context where the Command Pattern can be applied effectively.
By implementing the Command Pattern, software engineers can gain control over the execution of commands, encourage separation of concerns and improved code maintainability. If you're interested in more learning opportunities, subscribe to my free weekly newsletter and check out my YouTube channel!
Affiliations
These are products & services that I trust, use, and love. I get a kickback if you decide to use my links. There’s no pressure, but I only promote things that I like to use!
- BrandGhost: My social media content and scheduling tool that I use for ALL of my content!
- RackNerd: Cheap VPS hosting options that I love for low-resource usage!
- Contabo: Affordable VPS hosting options!
- ConvertKit: The platform I use for my newsletter!
- SparkLoop: Helps add value to my newsletter!
- Opus Clip: Tool for creating short-form videos!
- Newegg: For all sorts of computer components!
- Bulk Supplements: Huge selection of health supplements!
- Quora: I answer questions when folks request them!