Object Oriented Design - Chain of Responsibility Pattern
The Chain of Responsibility Pattern is a behavioral design pattern that allows an object to send a command without knowing which object will handle the request. A set of objects is chained in sequence, and the command is passed along the chain until one of the objects handle it.
How it Works?
- The client sends a request to the first handler in the chain.
- Each handler has the opportunity to process the request. It can either handle it or pass it along the chain.
- If there is no handler in the chain that handles the request, it either falls off the end of the chain or a default handler can provide a catch-all solution.
Design an ATM Machine Currency Dispenser
Designing an ATM machine's currency dispenser using the Chain of Responsibility pattern involves creating a series of handlers, each responsible for dispensing a specific denomination of currency. This design allows for efficient and flexible handling of various withdrawal amounts.
First, define an abstract class or interface for the currency dispenser:
abstract class CurrencyDispenser {
protected CurrencyDispenser nextDispenser;
public void setNextDispenser(CurrencyDispenser nextDispenser) {
this.nextDispenser = nextDispenser;
}
public abstract void dispenser(int amount);
}
Next, create concrete classes for each denomination:
class Rupees500Dispenser extends CurrencyDispenser {
@Override
public void dispense(final int amount) {
if (amount >= 500) {
int num = amount / 500;
int remaider = amount % 500;
if (remainder != 0) {
this.dispenser.dispense(remainder);
}
} else {
this.nextDispenser.dispense(amount);
}
}
}
class Rupees100Dispenser extends CurrencyDispenser {
@Override
public void dispense(final int amount) {
if (amount >= 100) {
int num = amount / 100;
int remaider = amount % 100;
if (remainder != 0) {
this.dispenser.dispense(remainder);
}
} else {
this.nextDispenser.dispense(amount);
}
}
}
Finally, set up the chain at the client.
public class ATMDispenserChain {
private CurrencyDispenser chain;
public ATMDispenserChain() {
this.chain = new Rupees500Dispenser();
final CurrencyDispenser rupees200 = new Rupees200Dispenser();
final CurrencyDispenser rupees100 = new Rupees100Dispenser();
chain.setNextDispenser(rupees200);
rupees200.setNextDispenser(rupees100);
}
public void dispense(final int amount) {
chain .dispense(amount);
}
}
How does Chain of Responsibility Pattern follow SOLID Principles?
The Chain of Responsibility pattern aligns well with the SOLID principles, which are guidelines for designing maintainable and extendable object-oriented software. Let's examine how it adheres to each of the SOLID principles:
1. Single Responsibility Principle (SRP)
- Each handler in the chain is responsible for a specific task or decision.
- In the ATM Currency Dispenser example, each dispenser is responsible only for handling its specific denomination.
2. Open/Closed Principle (OCP)
- It allows new types of handlers to be added without modifying the existing code. You can extend the chain by adding new handler classes that implement the same handler interface
- In the above example, new denominations can be added without altering existing code.
3. Liskov Substitution Principle (LSP)
- The Handlers are typically derived from a common handler interface or an abstract class. It allows any subclass to be substituted for their parent class (i.e., any specific handler can replace another in the chain).
- All dispensers (Rupees500Dispenser, Rupees200Dispenser.....) are interchangeable and derived from a common abstract class (CurrencyDispenser).
4. Interface Segregation Principle (ISP)
- It usually involves a simple interface for handling requests, often with a single method like
handle()
. This design ensures that implementing classes (handlers) are not forced to depend on methods they do not use. - The dispensers use a simple interface that doesn't enforce unnecessary methods and only has the
dispense()
method
5. Dependency Inversion Principle (DIP)
- The high-level modules (like the client code creating and using the chain) are not dependent on the low-level modules (the individual handlers). Instead, both depend on abstractions.
- High-level modules (
ATMDispenserChain
) depend on theCurrencyDispenser
abstract class, not on concrete implementations, aligning with DIP.
Design Examples for Chain of Responsibility Pattern
1. User Input Validation System
- Use Case: Validating various fields in a web form, such as email, password strength, and phone numbers.
- Application: Each handler in the chain is responsible for validating a specific form field or type of input. The chain processes each input field sequentially, ensuring all validations are met before the form is submitted.
2. Logging System
- Use Case: Notify the entry and exit gates or display boards when parking spots are occupied or become available.
- Application: Parking spots are subjects, and gates or display boards are observers.
3. Ticket Resolution System for a Big E-commerce Company
- Use Case: Routing customer support tickets to the appropriate department based on the issue type.
- Application: Handlers in the chain are responsible for different types of issues, ensuring that each ticket is directed to the correct department or personnel for resolution.
4. Hotel Reservation
- Use Case: Managing the steps involved in hotel booking and reservation.
- Application: Handlers in the chain take care of different steps like checking room availability, booking, and sending confirmation.
5. E-commerce Discount Processing
- Use Case: Implement a discount processing system in an e-commerce application where a purchase request is subjected to various discount handlers such as coupon code validation, membership discount application, and seasonal discount calculation.
- Application: Eeach handler in the chain is a specific discount type. The purchase request is passed through the chain, with each handler applying its respective discount if the conditions are met.
6. Authentication and Authorization Pipeline
- Use Case: Design an authentication and authorization system where a user request undergoes multiple security checks. These checks include token validation, role-based access control (RBAC), and rate limiting to ensure secure and controlled access to resources.
- Application: Each security check is encapsulated in a handler. The first handler in the chain validates the user's token, ensuring the user is authenticated. If the token is valid, the request moves to the next handler, which checks the user's roles and permissions (RBAC). After successful role validation, the request proceeds to the rate limiting handler, which ensures that the request rate conforms to defined limits