DEV Community

Cover image for Command Pattern in C#
Kostas Kalafatis
Kostas Kalafatis

Posted on • Originally published at dfordebugging.wordpress.com

Command Pattern in C#

The Command is a behavioral design pattern that converts a request into a stand-alone object that has all of the request's details. This transformation allows you to give requests as method arguments, postpone or queue the execution of a request, and support undoable operations.

You can find the example code of this post on GitHub

Conceptualizing the Problem

Assume we're developing a new text editor app. Our current objective is to design a toolbar with a slew of buttons for various editor tasks. We created a handy Button class that can be used for toolbar buttons as well as generic buttons in other dialogues.

Image description

Although these buttons appear to be the same, they have different purposes. Where should each button's click code be saved? The most obvious option is to create subclasses for each button's location that contain the code that will be executed when the button is clicked.

Image description

This approach has significant downsides. One significant difficulty is that it produces a high number of subclasses. Changes to the Button class are still manageable, but they may break the code in these subclasses. Essentially, your GUI code becomes overly reliant on the erratic business logic code.

Image description

And now comes the worst part. Some tasks, such as publishing the post, would need numerous calls. A user may, for example, click a little "Publish" button on the toolbar, click something from the context menu, or simply press Ctrl+Shift+P on the keyboard.

When our app only had the toolbar, it was fine to put the implementation of various operations in the button subclasses. In other words, having the publishing code inside the PublishButton subclass was sufficient. However, when you integrate context menus, shortcuts, and other features, you must either repeat the operation's code over many classes or make menus reliant on buttons, which is a worse alternative.

Good software design is frequently built on the separation of concerns approach, which usually results in the division of an app into layers. The most obvious example is a graphical user interface layer and a business logic layer. The GUI layer is in charge of displaying a nice image on the screen, taking any input, and displaying the outcomes of what the user and the app are doing. When it comes to accomplishing something significant, such as calculating the moon's trajectory or writing an annual report, the GUI layer outsources the task to the underlying layer of business logic.

It can look like this in the code: a GUI object calls a method of a business logic object, providing it with certain arguments. This is typically defined as one thing submitting a request to another.

Image description

According to the Command pattern, GUI objects should not submit these requests directly. Instead, we should isolate all request data, such as the object being called, the method name, and the list of arguments, into a distinct command class with a single function that triggers this request.

Command objects connect several GUI and business logic elements. The GUI object no longer needs to know which business logic object will receive the request and how it will be processed. The GUI object just initiates the command, which handles all of the details.

Image description

The next step is to ensure that all of our instructions use the same interface. It typically has a single execution method that takes no parameters. This interface allows us to utilize many commands with the same request sender without tying it to specific command classes. As an added bonus, we may now switch command objects associated with the sender, thus modifying its behavior at runtime.

You may have noticed that one piece of the puzzle is missing: the request parameters. A GUI object may have provided certain parameters to the business-layer object. How would we provide the request data to the receiver if the command execution method has no parameters? It turns out that the command should either be pre-configured with this information or capable of obtaining it on its own.

Image description

Let us return to our text editor. We no longer need all those button subclasses to implement various click actions after we apply the Command pattern. It is sufficient to add a single field to the base Button class that stores a reference to a command object and instructs the button to execute that command when clicked.

We'll create a slew of command classes for every potential function and associate them with certain buttons based on their intended behaviour.

Menus, shortcuts, and whole dialogues, for example, can be implemented in the same way. They will be linked to a command that will be executed when the user interacts with the GUI element. As you may expect, components connected to the same actions will be linked to the same commands, preventing code duplication.

As a result, commands serve as a useful intermediary layer, reducing the connection between the GUI and business logic levels. And that's only a taste of what the Command pattern has to offer.

Structuring the Command Pattern

In the basic implementation, the Command pattern has 5 main participants:

  • Invoker: Requests are initiated by the Invoker class. A field must be included in the class to store a reference to a command object. Rather than transmitting the request immediately to the receiver, the Invoker initiates that command. It is critical to note that the Invoker does not build the command object; rather, it receives an already-created command from the client via the constructor.
  • Command: The Command interface declares a single method for executing the command.
  • Concrete Commands: The different forms of requests are implemented via the Concrete Commands. A concrete command does not execute the job on its own; it simply forwards the request to one of the business logic classes. Fields in the concrete command can be declared to hold parameters required to perform a method on business logic objects. We can make these objects immutable by only allowing constructor-based field initialization.
  • Receiver: Some business logic is contained in the Receiver class. Almost any object can be used as a receiver. Most commands merely deal with the mechanics of how a request is transmitted to the receiver, leaving the receiver to conduct the actual job.
  • Client: The Client is in charge of creating and configuring concrete command objects. The client must supply the command's constructor all of the request parameters, including a receiver instance. The resulting command can then be associated with one or more senders.

Image description

In order to demonstrate how the Command pattern works, we are going to implement a real-life example. Assume we want to provide a File System utility containing methods for opening, writing, and closing files. This file system application should be compatible with a variety of operating systems, including Windows and Unix.

First we are going to implement our Receiver classes. We are first going to define an IFileSystemReceiver interface, which will be the base of all of our receivers. Remember that our Receiver participants deal with the actual implementation of the business logic.

public interface IFileSystemReceiver
{
    public void OpenFile();
    public void WriteFile();
    public void CloseFile();
}
Enter fullscreen mode Exit fullscreen mode

Our interface defines three methods, for opening, writing and closing a file. Now that we have our interface, we are going to implement our Concrete Receiver classes. First, the UnixFileSystemReceiver:

public class UnixFileSystemReceiver : IFileSystemReceiver
{
    public void OpenFile()
    {
        Console.WriteLine("Opening file in Unix OS");
    }

    public void WriteFile()
    {
        Console.WriteLine("Writing file in Unix OS");
    }

    public void CloseFile()
    {
        Console.WriteLine("Closing file in Unix OS");
    }
}
Enter fullscreen mode Exit fullscreen mode

And the WindowsFileSystemReceiver class:

public class WindowsFileSystemReceiver : IFileSystemReceiver
{
    public void OpenFile()
    {
        Console.WriteLine("Opening file in Windows OS");
    }

    public void WriteFile()
    {
        Console.WriteLine("Writing file in Windows OS");
    }

    public void CloseFile()
    {
        Console.WriteLine("Closing file in Windows OS");
    }
}
Enter fullscreen mode Exit fullscreen mode

The next step is to define our Command participant. The Command participant usually defines a single method for executing the command. In our case, we are going to define our ICommand interface:

public interface ICommand
{
    public void Execute();
}
Enter fullscreen mode Exit fullscreen mode

Now we must write implementations for each type of action done by the receiver. We'll make three ConcreteCommand implementations because we have three actions. Each Concrete Command implementation will route the request to the proper receiver method. First, the OpenFileCommand:

public class OpenFileCommand : ICommand
{
    private readonly IFileSystemReceiver _fileSystemReceiver;

    public OpenFileCommand(IFileSystemReceiver fileSystemReceiver)
    {
        _fileSystemReceiver = fileSystemReceiver;
    }

    public void Execute()
    {
        _fileSystemReceiver.OpenFile();
    }
}
Enter fullscreen mode Exit fullscreen mode

Next up, the CloseFileCommand:

public class CloseFileCommand : ICommand
{
    private readonly IFileSystemReceiver _fileSystemReceiver;

    public CloseFileCommand(IFileSystemReceiver fileSystemReceiver)
    {
        _fileSystemReceiver = fileSystemReceiver;
    }

    public void Execute()
    {
        _fileSystemReceiver.CloseFile();
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, the WriteFileCommand:

public class WriteFileCommand : ICommand
{
    private readonly IFileSystemReceiver _fileSystemReceiver;

    public WriteFileCommand(IFileSystemReceiver fileSystemReceiver)
    {
        _fileSystemReceiver = fileSystemReceiver;
    }

    public void Execute()
    {
        _fileSystemReceiver.WriteFile();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we've completed the receiver and command implementations, we can move on to the invoker class. Instead of delivering the request immediately to the receiver, the Invoker initiates that command. It should be noted that the sender is not in charge of constructing the command object. Below is our FileInvoker class:

public class FileInvoker
{
    private readonly ICommand _command;

    public FileInvoker(ICommand command)
    {
        this._command = command;
    }

    public void Execute()
    {
        _command.Execute();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we've completed our command implementation, we can move on to writing a simple client program. But first, we'll need to provide a utility method for creating the necessary IFileSystemReceiver objects. This method will peek on the underlying operating system and decide which subclass of the IFileSystemReceiver to instantiate:

public class FileSystemReceiverUtils
{
    public static IFileSystemReceiver GetFileSystemReceiver()
    {
        var osName = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
        Console.WriteLine($"Underlying OS: {osName}");

        if (osName.Contains("Windows"))
            return new WindowsFileSystemReceiver();

        return new UnixFileSystemReceiver();
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally let's implement our Client participant:

var fs = FileSystemReceiverUtils.GetFileSystemReceiver();

var openFileCommand = new OpenFileCommand(fs);
var invoker = new FileInvoker(openFileCommand);
invoker.Execute(); 

var writeFileCommand = new WriteFileCommand(fs);
invoker = new FileInvoker(writeFileCommand);
invoker.Execute(); 

var closeFileCommand = new CloseFileCommand(fs);
invoker = new FileInvoker(closeFileCommand);
invoker.Execute(); 
Enter fullscreen mode Exit fullscreen mode

If we run our client, we will get the following output:
![[command-output.png]]

Pros and Cons of the Command Pattern

✔ Classes that invoke operations can be separated from classes that conduct these actions, thus respecting the Single Responsibility Principle. ❌Because we're introducing a new layer between senders and receivers, the code may become more complicated.
✔We can add new commands to the app without affecting the existing client code, thus respecting the Open/Closed principle.
✔We can use deferred execution of operations..

Relations with Other Patterns

  • Chain of Responsibility, Command, Mediator, and Observer address several methods of connecting request senders and receivers:
    • Chain of Responsibility routes a request sequentially through a dynamic chain of potential recipients until it is handled by one of them.
    • Command creates one-way relationships between senders and receivers.
    • Mediator disables direct connections between senders and receivers, forcing them to communicate through a mediator object.
    • Receivers can dynamically subscribe to and unsubscribe from receiving requests using Observer.
  • Commands can be implemented as handlers in the Chain of Responsibility. In this scenario, you can perform a variety of operations on the same context object, which is represented by a request. However, there is another approach in which the request is a Command object in and of itself. In this situation, you can carry out the same operation in a chain of various contexts.
  • When implementing "undo," you can use Command and Memento together. In this situation, commands are in charge of conducting various operations on a target object, whereas mementos record the object's state right before a command is executed.
  • Because both Command and Strategy can be used to parameterize an object with an action, they may appear similar. They do, however, have very distinct intentions.
    • Any operation can be converted into an object using Command. The parameters of the procedure become fields of that object. The conversion allows you to postpone the operation's execution, queue it, save command history, transmit commands to distant services, and so on.
    • Strategy, on the other hand, typically defines multiple ways of achieving the same thing, allowing you to switch these algorithms within a single context class.
  • When you need to save copies of Commands into history, Prototype can help.
  • Visitor can be thought of as a more powerful variant of the Command pattern. Its objects can perform operations on objects of various classes.

Final Thoughts

In this article, we have discussed what is the Command pattern, when to use it and what are the pros and cons of using this design pattern. We then examined some use cases for this pattern and how the Command relates to other classic design patterns.

It's worth noting that the Command pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.

Top comments (1)

Collapse
 
michaeltharrington profile image
Michael Tharrington

Super helpful post in an excellent series, thanks for sharing Kostas!