At my workplace, we often deal with complex business logic that involves multiple operations. To maintain a clean and maintainable codebase, we decided to implement the Command Design Pattern. This pattern not only improved our code structure but also enhanced its extensibility and scalability. In this blog, I'll walk you through the Command Design Pattern, compare code written without and with this pattern, and discuss its advantages using a real-world example of updating organization details.
What is the Command Design Pattern?#
The Command Design Pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation allows us to parameterize methods with different requests, delay or queue a request's execution, and support undoable operations.
Code Without Command Pattern#
Let's consider a simple example where we need to update the details of an organization. Here's how the code might look without using the Command Pattern:
// Organization class
public class Organization {
private String name;
private String address;
public Organization(String name, String address) {
this.name = name;
this.address = address;
}
public void updateName(String newName) {
this.name = newName;
System.out.println("Organization name updated to: " + newName);
}
public void updateAddress(String newAddress) {
this.address = newAddress;
System.out.println("Organization address updated to: " + newAddress);
}
// Getters for name and address
}
// OrganizationService class
public class OrganizationService {
private Organization organization;
public OrganizationService(Organization organization) {
this.organization = organization;
}
public void updateDetails(String newName, String newAddress) {
organization.updateName(newName);
organization.updateAddress(newAddress);
}
}
// Main class
public class Main {
public static void main(String[] args) {
Organization org = new Organization("Old Name", "Old Address");
OrganizationService orgService = new OrganizationService(org);
orgService.updateDetails("New Name", "New Address");
}
}
In this implementation, the OrganizationService
class directly depends on the Organization
class and its specific methods. This tight coupling makes the code difficult to extend and maintain, especially when new operations are introduced.
Code With Command Pattern#
By implementing the Command Pattern, we can decouple the invoker (organization service) from the receiver (organization) and encapsulate the request as an object. Here's how we can refactor the above code:
// Command interface
public interface Command {
void execute();
}
// Organization class
public class Organization {
private String name;
private String address;
public Organization(String name, String address) {
this.name = name;
this.address = address;
}
public void updateName(String newName) {
this.name = newName;
System.out.println("Organization name updated to: " + newName);
}
public void updateAddress(String newAddress) {
this.address = newAddress;
System.out.println("Organization address updated to: " + newAddress);
}
// Getters for name and address
}
// Concrete Command classes
public class UpdateNameCommand implements Command {
private Organization organization;
private String newName;
public UpdateNameCommand(Organization organization, String newName) {
this.organization = organization;
this.newName = newName;
}
@Override
public void execute() {
organization.updateName(newName);
}
}
public class UpdateAddressCommand implements Command {
private Organization organization;
private String newAddress;
public UpdateAddressCommand(Organization organization, String newAddress) {
this.organization = organization;
this.newAddress = newAddress;
}
@Override
public void execute() {
organization.updateAddress(newAddress);
}
}
// OrganizationService class
public class OrganizationService {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void executeCommand() {
command.execute();
}
}
// Main class
public class Main {
public static void main(String[] args) {
Organization org = new Organization("Old Name", "Old Address");
Command updateName = new UpdateNameCommand(org, "New Name");
Command updateAddress = new UpdateAddressCommand(org, "New Address");
OrganizationService orgService = new OrganizationService();
orgService.setCommand(updateName);
orgService.executeCommand();
orgService.setCommand(updateAddress);
orgService.executeCommand();
}
}
In this refactored implementation, we have introduced a Command
interface and concrete command classes (UpdateNameCommand
and UpdateAddressCommand
). The OrganizationService
class now uses a command object to perform operations, which decouples it from the specific implementations of those operations.
Advantages of Using the Command Pattern#
-
Decoupling of Invoker and Receiver: The invoker (organization service) does not need to know the specifics of the receiver (organization). It only interacts with the command interface, making the code more flexible and easier to extend.
-
Extensibility: Adding new commands is straightforward. We just need to implement a new command class without modifying existing code.
-
Support for Undo Operations: By storing executed commands, we can implement undo functionality. Each command can have an
unexecute
method to reverse its action. -
Queue and Log Requests: Commands can be queued or logged for future execution, enabling features like request logging, job scheduling, and task retry mechanisms.
-
Promotes Reusability: Common commands can be reused across different parts of the application, reducing code duplication.
Conclusion#
Implementing the Command Design Pattern at my workplace has significantly improved our code's maintainability and extensibility. By decoupling the invoker from the receiver and encapsulating requests as objects, we have made our codebase more flexible and easier to manage. If you're dealing with complex operations in your projects, consider using the Command Pattern to achieve a cleaner and more modular design.