1. Overview

The command pattern is a behavioral design pattern and is part of the GoF‘s formal list of design patterns. Simply put, the pattern intends to encapsulate in an object all the data required for performing a given action (command), including what method to call, the method’s arguments, and the object to which the method belongs.

This model allows us to decouple objects that produce the commands from their consumers, so that’s why the pattern is commonly known as the producer-consumer pattern.

In this tutorial, we’ll learn how to implement the command pattern in Java by using both object-oriented and object-functional approaches, and we’ll see in what use cases it can be useful.

2. Object-Oriented Implementation

In a classic implementation, the command pattern requires implementing four components: the Command, the Receiver, the Invoker, and the Client.

To understand how the pattern works and the role that each component plays, let’s create a basic example.

Let’s suppose that we want to develop a text file application. In such a case, we should implement all functionality required for performing some text-file related operations, such as opening, writing, saving a text file, and so forth.

So, we should break down the application into the four components mentioned above.

2.1. Command Classes

A command is an object whose role is to store all the information required for executing an action, including the method to call, the method arguments, and the object (known as the receiver) that implements the method.

To get a more accurate idea of how command objects work, let’s start developing a simple command layer which includes just one single interface and two implementations:

@FunctionalInterface
public interface TextFileOperation {
    String execute();
}
public class OpenTextFileOperation implements TextFileOperation {

    private TextFile textFile;
    
    // constructors
    
    @Override
    public String execute() {
        return textFile.open();
    }
}
public class SaveTextFileOperation implements TextFileOperation {
    
    // same field and constructor as above
        
    @Override
    public String execute() {
        return textFile.save();
    }
}

In this case, the TextFileOperation interface defines the command objects’ API, and the two implementations, OpenTextFileOperation and SaveTextFileOperation, perform the concrete actions. The former opens a text file, while the latter saves a text file.

It’s clear to see the functionality of a command object: the TextFileOperation commands encapsulate all the information required for opening and saving a text file, including the receiver object, the methods to call, and the arguments (in this case, no arguments are required, but they could be).

It’s worth stressing that the component that performs the file operations is the receiver (the TextFile instance).

2.2. The Receiver Class

A receiver is an object that performs a set of cohesive actions. It’s the component that performs the actual action when the command’s execute() method is called.

In this case, we need to define a receiver class, whose role is to model TextFile objects:

public class TextFile {
    
    private String name;
    
    // constructor
    
    public String open() {
        return "Opening file " + name;
    }
    
    public String save() {  
        return "Saving file " + name;
    }
    
    // additional text file methods (editing, writing, copying, pasting)
}

2.3. The Invoker Class

An invoker is an object that knows how to execute a given command but doesn’t know how the command has been implemented. It only knows the command’s interface.

In some cases, the invoker also stores and queues commands, aside from executing them. This is useful for implementing some additional features, such as macro recording or undo and redo functionality.

In our example, it becomes evident that there must be an additional component responsible for invoking the command objects and executing them through the commands’ execute() method. This is exactly where the invoker class comes into play.

Let’s look at a basic implementation of our invoker:

public class TextFileOperationExecutor {
    
    private final List<TextFileOperation> textFileOperations
     = new ArrayList<>();
    
    public String executeOperation(TextFileOperation textFileOperation) {
        textFileOperations.add(textFileOperation);
        return textFileOperation.execute();
    }
}

The TextFileOperationExecutor class is just a thin layer of abstraction that decouples the command objects from their consumers and calls the method encapsulated within the TextFileOperation command objects.

In this case, the class also stores the command objects in a List. Of course, this is not mandatory in the pattern implementation, unless we need to add some further control to the operations’ execution process.

2.4. The Client Class

A client is an object that controls the command execution process by specifying what commands to execute and at what stages of the process to execute them.

So, if we want to be orthodox with the pattern’s formal definition, we must create a client class by using the typical main method:

public static void main(String[] args) {
    TextFileOperationExecutor textFileOperationExecutor
      = new TextFileOperationExecutor();
    textFileOperationExecutor.executeOperation(
      new OpenTextFileOperation(new TextFile("file1.txt"))));
    textFileOperationExecutor.executeOperation(
      new SaveTextFileOperation(new TextFile("file2.txt"))));
}

3. Object-Functional Implementation

So far, we’ve used an object-oriented approach to implement the command pattern, which is all well and good.

From Java 8, we can use an object-functional approach, based on lambda expressions and method references, to make the code a little bit more compact and less verbose.

3.1. Using Lambda Expressions

As the TextFileOperation interface is a functional interface, we can pass command objects in the form of lambda expressions to the invoker, without having to create the TextFileOperation instances explicitly:

TextFileOperationExecutor textFileOperationExecutor
 = new TextFileOperationExecutor();
textFileOperationExecutor.executeOperation(() -> "Opening file file1.txt");
textFileOperationExecutor.executeOperation(() -> "Saving file file1.txt");

The implementation now looks much more streamlined and concise, as we’ve reduced the amount of boilerplate code.

Even so, the question still stands: is this approach better, compared to the object-oriented one?

Well, that’s tricky. If we assume that more compact code means better code in most cases, then indeed it is.

As a rule of thumb, we should evaluate on a per-use-case basis when to resort to lambda expressions.

3.2. Using Method References

Similarly, we can use method references for passing command objects to the invoker:

TextFileOperationExecutor textFileOperationExecutor
 = new TextFileOperationExecutor();
TextFile textFile = new TextFile("file1.txt");
textFileOperationExecutor.executeOperation(textFile::open);
textFileOperationExecutor.executeOperation(textFile::save);

In this case, the implementation is a little bit more verbose than the one that uses lambdas, as we still had to create the TextFile instances.

4. Conclusion

In this article, we learned the command pattern’s key concepts and how to implement the pattern in Java by using an object-oriented approach and a combination of lambda expressions and method references.

As usual, all the code examples shown in this tutorial are available over on GitHub.