Skip to main content

Command Design Pattern

Problem​

You: Hey Shubh, how can we implement code to transfer money from one person to another?

Shubh: We can create a function transfer(sender, receiver, amount) and write the logic to handle the money transfer.

You: And what about implementing code to transfer money from my checking account to my savings account?

Shubh: We can create a similar function transfer(account1, account2, amount) to handle the transfer.

You: What if I want to undo a payment to another person?

Shubh: Oh, that's a bit complicated. We would need to track each transaction's details to reverse it, which could make the code more complex and harder to manage.

Solution​

You: So how can we simplify this?

Shubh: By using the Command Design Pattern. We'll create command objects like PaymentCommand and TransferCommand. Each command encapsulates all the details of the operation. The client code won't need to manage the complexity of the transaction logic. Instead, each command can have its own execute() and undo() methods.

You: How does that help with undoing a payment?

Shubh: Each command knows how to reverse its action. For example, the PaymentCommand can swap the sender and receiver to undo a payment, effectively making the code cleaner and more manageable.

You: This sounds neat and organized.

UML​

Command Design Pattern

UML for Command Design Pattern

  • Receiver: This is the class on which the request will be executed, such as a bank account in our example.

  • Command: This interface defines the execute() and undo() methods. Each command object implements these methods to perform specific actions.

  • Concrete Command: These are the implementations of the Command interface (e.g., PaymentCommand, TransferCommand). They encapsulate all the details of a request.

  • Invoker: This class, such as a TransactionManager, tracks all the commands executed so far and is responsible for executing or undoing the latest command.

Code Implementation​

BankAccount Class​

BankAccount.java
class BankAccount {
private double balance;
private String accountName;

public BankAccount(String accountName, double balance) {
this.accountName = accountName;
this.balance = balance;
}

public void deposit(double amount) {
balance += amount;
}

public void withdraw(double amount) {
if (amount > balance) {
System.out.println("Insufficient funds in " + accountName);
} else {
balance -= amount;
}
}

public double getBalance() {
return balance;
}

public String getAccountName() {
return accountName;
}
}

Command Interface​

Command.java
public interface Command {
void execute();
void undo();
}

PaymentCommand Class​

PaymentCommand.java
class PaymentCommand implements Command {
private BankAccount sender;
private BankAccount receiver;
private double amount;

public PaymentCommand(BankAccount sender, BankAccount receiver, double amount) {
this.sender = sender;
this.receiver = receiver;
this.amount = amount;
}

@Override
public void execute() {
if (amount > sender.getBalance()) {
System.out.println("Insufficient funds in " + sender.getAccountName());
} else {
sender.withdraw(amount);
receiver.deposit(amount);
System.out.println("Transferred " + amount + " from " + sender.getAccountName() + " to " + receiver.getAccountName());
}
}

@Override
public void undo() {
receiver.withdraw(amount);
sender.deposit(amount);
System.out.println("Reversed payment of " + amount + " from " + receiver.getAccountName() + " back to " + sender.getAccountName());
}
}

TransferCommand Class​

TransferCommand.java
class TransferCommand implements Command {
private BankAccount fromAccount;
private BankAccount toAccount;
private double amount;

public TransferCommand(BankAccount fromAccount, BankAccount toAccount, double amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}

@Override
public void execute() {
if (amount > fromAccount.getBalance()) {
System.out.println("Insufficient funds in " + fromAccount.getAccountName());
} else {
fromAccount.withdraw(amount);
toAccount.deposit(amount);
System.out.println("Transferred " + amount + " from " + fromAccount.getAccountName() + " to " + toAccount.getAccountName());
}
}

@Override
public void undo() {
toAccount.withdraw(amount);
fromAccount.deposit(amount);
System.out.println("Reversed transfer of " + amount + " from " + toAccount.getAccountName() + " back to " + fromAccount.getAccountName());
}
}

TransactionManager Class (Invoker)​

TransactionManager.java
import java.util.Stack;

class TransactionManager {
private Command command;
private Stack<Command> commandHistory = new Stack<>();

public void setCommand(Command command) {
this.command = command;
}

public void executeCommand() {
command.execute();
commandHistory.push(command);
}

public void undoLastCommand() {
if (!commandHistory.isEmpty()) {
Command command = commandHistory.pop();
command.undo();
} else {
System.out.println("No commands to undo");
}
}
}

Main Class (Client Code)​

Main.java
public class Main {
public static void main(String[] args) {
BankAccount checking = new BankAccount("Checking Account", 500);
BankAccount savings = new BankAccount("Savings Account", 200);
BankAccount friend = new BankAccount("Friend's Account", 100);

TransactionManager transactionManager = new TransactionManager();

// Transfer money from checking to savings
TransferCommand transferCommand = new TransferCommand(checking, savings, 100);
transactionManager.setCommand(transferCommand);
transactionManager.executeCommand();
System.out.println("Checking balance: " + checking.getBalance()); // 400
System.out.println("Savings balance: " + savings.getBalance()); // 300

// Undo the transfer
transactionManager.undoLastCommand();
System.out.println("Checking balance: " + checking.getBalance()); // 500
System.out.println("Savings balance: " + savings.getBalance()); // 200

// Pay money to a friend
PaymentCommand paymentCommand = new PaymentCommand(checking, friend, 50);
transactionManager.setCommand(paymentCommand);
transactionManager.executeCommand();
System.out.println("Checking balance: " + checking.getBalance()); // 450
System.out.println("Friend's balance: " + friend.getBalance()); // 150

// Undo the payment
transactionManager.undoLastCommand();
System.out.println("Checking balance: " + checking.getBalance()); // 500
System.out.println("Friend's balance: " + friend.getBalance()); // 100
}
}

Key Benefits of the Command Design Pattern​

Before watching, make sure you give it a try
  1. Decoupling: The Command Pattern decouples the sender of a request from its receiver, making the system more modular and flexible.

  2. Undo Functionality: By encapsulating requests as objects, the Command Pattern makes it easy to implement undo functionality. Each command knows how to undo its own action.

  3. Queueing and Logging: Commands can be queued, logged, or executed at a later time. This is useful for scenarios like task scheduling or implementing a history of actions.

  4. Extensibility: New commands can be added without changing existing code. You can simply create new command classes that implement the Command interface.

  5. Parameterization of Objects: Commands allow objects to be parameterized with different requests, enabling dynamic behavior.