How to Leverage the Command Pattern for Better Decoupling

Kyle Galbraith - Oct 15 '18 - - Dev Community

When it comes to programming patterns, the command pattern is one that can take a bit to wrap your head around. But once you understand the components at play and the simplicity in which they can be implemented, it can be a real game changer to your coding.

To summarize, the command pattern is a programming pattern that allows us to encapsulate requests as objects. This idea of boiling requests into objects that share an interface affords us a lot of different benefits. The two that I view as the most valuable are:

  • One request processor can process all the various types of requests we have.
  • Each type of request can be processed in isolation.

These are the benefits that we are going to explore throughout this post while focusing on our example. Let's establish a baseline for when the command pattern might be worth your time to implement.

Knowing when to use the command pattern

Because the command pattern is great for processing a collection of requests, it is key to look at the code that is processing more than one type of request. For example, let's say we have these two request types ExtractPdf and ExtractWord.

public class ExtractPdf
{
    private readonly string _documentPath;
    private readonly string _documentName;
    private readonly string _pdfVersion;

    public ExtractPdf(string documentPath, string documentName, string pdfVersion)
    {
        _documentPath = documentPath;
        _documentName = documentName;
        _pdfVersion = pdfVersion;
    }

    public string GetPdfContent()
    {
        // logic to extract the given pdf file.
    }
}
Enter fullscreen mode Exit fullscreen mode

Here the ExtractPdf request class has a constructor that takes three arguments and a method, GetPdfContent(), that has the logic to extract the contents of the PDF file.

Taking a look at the ExtractWord request class we start to see some similarities.

public class ExtractWord
{
    private readonly string _documentPath;
    private readonly string _documentName;

    public ExtractWord(string documentPath, string documentName)
    {
        _documentPath = documentPath;
        _documentName = documentName;
    }

    public string GetWordContent()
    {
        // logic to extract the given word file.
    }
}
Enter fullscreen mode Exit fullscreen mode

Here the ExtractWord class has a constructor that takes two arguments and its own extraction method, GetWordContent(). What we have here is a common interface that the two request classes share, they both extract content from a file type.

Let's take a look at our RequestProcessor class that is responsible for processing these requests.

public class RequestProcessor
{
    public RequestProcessor()
    {    
    }

    public ProcessPdfRequests(ExtractPdf[] pdfRequests)
    {
        foreach(req in pdfRequests)
        {
            var content = req.GetPdfContent();
            // do something with the content...
        }
    }

    public ProcessWordRequests(ExtractWord[] wordRequests)
    {
        foreach(req in wordRequests)
        {
            var content = req.GetWordContent();
            // do something with the content...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Looking at our RequestProcessor class we see that we have a process function for each type of request. That function takes in an array of requests and loops over them to extract the content.

This is a prime example of a place where the command pattern can come in handy.

Why? Think about if you added another type of request like ExtractImage, what all would need to change to support that?

  1. We would need a new request class ExtractImage that implements the extraction logic.
  2. We would need to update the request processor to have a new function to process those types of requests.

The second point is the key one here because what this means is that additional request types cannot be added without touching the processing logic. Think of this as your sniff test.

If to add or modify an existing request requires changing the logic of my message processing, I likely could encapsulate my requests in a more generic way.

Let's leverage the command pattern

We can introduce the command pattern to this example code by first introducing a common interface that the requests are going to share.

Let's define the IRequestExtraction interface.

public interface IRequestExtraction
{
    string Extract();
}
Enter fullscreen mode Exit fullscreen mode

With the interface defined for our request interface, we can refactor our ExtractPdf and ExtractWord request classes to implement it.

public class ExtractPdf : IRequestExtraction
{
    private readonly string _documentPath;
    private readonly string _documentName;
    private readonly string _pdfVersion;

    public ExtractPdf(string documentPath, string documentName, string pdfVersion)
    {
        _documentPath = documentPath;
        _documentName = documentName;
        _pdfVersion = pdfVersion;
    }

    public string Extract()
    {
        // logic to extract the given pdf file.
    }
}
Enter fullscreen mode Exit fullscreen mode
public class ExtractWord : IRequestExtraction
{
    private readonly string _documentPath;
    private readonly string _documentName;

    public ExtractWord(string documentPath, string documentName)
    {
        _documentPath = documentPath;
        _documentName = documentName;
    }

    public string Extract()
    {
        // logic to extract the given word file.
    }
}
Enter fullscreen mode Exit fullscreen mode

We really only changed two things in our implementations of the two types of requests. We change the classes to implement the IRequestExtraction interface and we change the get content methods to be the Extract() method defined in the interface.

Now we can refactor our RequestProcessor to take advantage of this common interface.

public class RequestProcessor
{
    public RequestProcessor()
    {    
    }

    public ProcessRequests(IRequestExtraction[] requests)
    {
        foreach(req in requests)
        {
            var content = req.Extract();
            // do something with the content...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What we have done is removed any notion of the type of extraction our request processor supports. Instead, it now has one method, ProcessRequests, which takes an array of IRequestExtraction objects and executes their Extract method.

We have removed the need for the processor to know what type of requests it is processing. This allows it to take any object that meets the request interface and execute a command on it.

This allows us to add, remove, and change the implementation of requests/commands without having to modify the processor. As long as the extraction method returns the content, no changes are necessary.

In command pattern speak, we refer to this request processor as our Invoker because it is responsible for telling the commands to execute. The result of this execution could then be passed along to a Receiver which is going to do something with that content.

Wrapping up

The command pattern is very useful for systems in which we want to queue or batch up the processing of requests. It has the added benefit of also decoupling our request processing from the business logic that creates requests. This pattern assumes that you have more than one type of request that you want to process, so if that is not the case for you the unnecessary overhead might not be worth your time.

When we introduce this queuing of common requests represented by an interface we give ourselves the agility to add and remove types of messages. The requests remain isolated so that each can process an extraction as they see fit. But we made the processor more robust by not having to care about how the extraction needs to be done.

Hungry To Learn Amazon Web Services?

There is a lot of people that are hungry to learn Amazon Web Services. Inspired by this fact I have created a course focused on learning Amazon Web Services by using it. Focusing on the problem of hosting, securing, and delivering static websites. You learn services like S3, API Gateway, CloudFront, Lambda, and WAF by building a solution to the problem.

There is a sea of information out there around AWS. It is easy to get lost and not make any progress in learning. By working through this problem we can cut through the information and speed up your learning. My goal with this book and video course is to share what I have learned with you.

Sound interesting? Check out the landing page to learn more and pick a package that works for you, learn AWS basics by actually using it.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .