C# Design Patterns – The Command Pattern

Introduction

In the vast realm of C# design patterns, the Command Pattern stands out as a versatile and powerful tool. This pattern encapsulates a request as an object, allowing developers to parameterize objects with different requests, queue requests, and even log the requests. For .NET developers, mastering this pattern can lead to cleaner, more maintainable code.

In this article, we’ll get our hands into the Command Pattern, including a practical example for you to easily understand how it works.


What is the Command Pattern?: At its core, the Command Pattern is about creating a separation between the object that invokes an operation and the object that knows how to execute that operation. It encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, and the queuing of requests. It also provides support for undoable operations.


Why should we include the Command Pattern as part of our C# design patterns arsenal?

  1. Decoupling: It decouples the classes that invoke the operation from the object that knows how to execute the operation.
  2. Flexibility: It allows for the addition of new commands without changing existing code, promoting the Open/Closed Principle (SOLID).
  3. Undo/Redo: Supports undo and redo operations if needed.

Practical Example

Remember those good old menu driven console apps? Imagine a Library Application which needs a way to implement CRUD operations, this could potentially be done with the use of a switch statement, like this:

public void ExecuteOperation(string choice)
{
    switch (choice)
    {
        case "1":
            AddBook();
            break;
        case "2":
            RemoveBook();
            break;
        case "3":
            ListAllBooks();
            break;
        case "4":
            FindBookById();
            break;
        case "5":
            Console.WriteLine("Exiting the application.");
            return;
        default:
            Console.WriteLine("Invalid choice. Please try again.");
            break;
    }
}

Implementation is easy, but in these modern times, that looks a little like old school right? The list below displays some of the bad practices that the code emcompases:

  1. Violation of Open/Closed Principle: Adding a new operation requires modifying the ExecuteOperation method.
  2. Lack of Scalability: As operations grow, the switch statement becomes unwieldy.
  3. Tight Coupling: The method is closely tied to specific operations, reducing flexibility.
  4. Low Cohesion: The method handles both operation execution and user interaction.
  5. Testing Challenges: The structure makes unit testing individual operations difficult.

With the use of the Command Pattern, we can improve that code significantly, as you can see below:

public interface ICommand
{
    void Execute();
}

public class AddBookCommand : ICommand
{
    public void Execute()
    {
        // Implementation to add a book
    }
}

public class RemoveBookCommand : ICommand
{
    public void Execute()
    {
        // Implementation to remove a book
    }
}

// ... Similarly for other commands

By using commands, we can easily add new features or modify existing ones without affecting the core logic of our application.

We then create a dictionary of Commands, which will be used to match the choice you make with the proper Command operation:

Dictionary<string, ICommand> commands = new Dictionary<string, ICommand>
{
    { "1", new AddBookCommand() },
    { "2", new RemoveBookCommand() },
    // ... other commands
};

And finally, we can replace that old Switch statement with a code such as this:

public void ExecuteOperation(string choice)
{
    if (commands.ContainsKey(choice))
    {
        commands[choice].Execute();
    }
    else
    {
        Console.WriteLine("Invalid choice. Please try again.");
    }
}

Much more elegant, don’t you think? 😉

Benefits in the Real World

The Command Pattern offers a versatile approach to handling operations in software design, making it invaluable in diverse real-world applications. By encapsulating actions as objects, it promotes modularity and flexibility. This pattern is especially beneficial in systems that require undo/redo capabilities, delayed or scheduled tasks, and macro recording.

Additionally, it aids in distributed environments, allowing for operations to be serialized and transmitted across networks or balanced among servers. Furthermore, the Command Pattern enhances the traceability of actions, facilitating improved logging and auditing in complex systems. Overall, its adoption can lead to cleaner, more maintainable, and scalable applications across various domains.

I hope this simple explanation helps you to add another design pattern into your arsenal.

Happy coding!