What is the Chain of Responsibility Design Pattern?
The purpose of using the chain of responsibility design pattern is to create a chain of objects that can handle a request. This allows for a more modular and flexible approach to handling requests, as the objects in the chain are able to handle the request independently of each other. Additionally, the chain of responsibility pattern allows for the request to be handled by the object that is best suited to handle it, rather than having to rely on a single, monolithic object. This can improve the maintainability and extensibility of the code.
Why did the chain of responsibility object need therapy? It was tired of being passed from object to object without any appreciation for the work it did.
Imagine you want to borrow a friend’s toy. You ask your mom, but she says you have to ask your dad. So you go to your dad, and he says you have to ask your friend first. In this situation, each person is like an object in the chain of responsibility pattern, and they each have a chance to handle the request (in this case, to borrow the toy). If they can’t handle it, they pass it on to the next person in the chain until someone can handle it.
The chain of responsibility design pattern can be used in a variety of situations. For example, it can be used in event-driven systems, where events are generated by one object and need to be handled by one or more other objects. It can also be used in systems that process requests or commands, where the request or command needs to be handled by the appropriate object. Additionally, the pattern can be used in systems that use a hierarchical structure, where requests or commands need to be passed up or down the hierarchy to be handled. In general, the pattern can be used any time you want to create a chain of objects that can handle a request or command in a modular and flexible way.
To use the chain of responsibility design pattern in C#, you would first define an interface for the objects in the chain. This interface would specify the methods that the objects in the chain would need to implement in order to handle requests. For example, the interface might look like this:
public interface IHandler
{
bool HandleRequest(Request request);
}
Next, you would define the different classes that will make up the chain of responsibility. Each class would implement the IHandler interface and provide its own implementation of the HandleRequest method. This method would contain the logic for handling the request, and would either handle the request itself or pass it on to the next object in the chain. For example, one class in the chain might look like this:
public class FirstHandler : IHandler
{
private IHandler _nextHandler;
public void SetNextHandler(IHandler nextHandler)
{
_nextHandler = nextHandler;
}
public bool HandleRequest(Request request)
{
// Check if this object can handle the request.
if (CanHandleRequest(request))
{
// Handle the request.
return HandleRequestInternal(request);
}
else
{
// Pass the request on to the next object in the chain.
return _nextHandler.HandleRequest(request);
}
}
// Other methods...
}
Once you have defined the classes that make up the chain of responsibility, you can create an instance of the first object in the chain and set the next object in the chain for each object. This can be done using code like this:
var firstHandler = new FirstHandler();
var secondHandler = new SecondHandler();
var thirdHandler = new ThirdHandler();
firstHandler.SetNextHandler(secondHandler);
secondHandler.SetNextHandler(thirdHandler);
Once the objects are set up in the correct order, you can call the HandleRequest method on the first object in the chain to handle the request. If the request can be handled by any of the objects in the chain, it will be handled and true will be returned. Otherwise, the request will be passed all the way down the chain without being handled and false will be returned.
Here is a simple example of the chain of responsibility design pattern using C#. This example simulates a hotel booking system where a customer can request a booking and the request is handled by a chain of handlers, each one responsible for a specific type of room:
// Abstract base class for handlers
abstract class BookingHandler
{
protected BookingHandler nextHandler;
public void SetNext(BookingHandler handler)
{
nextHandler = handler;
}
public abstract void HandleBooking(Booking booking);
}
// Concrete handler for handling standard room bookings
class StandardRoomHandler : BookingHandler
{
public override void HandleBooking(Booking booking)
{
if (booking.Type == RoomType.Standard)
{
// handle booking for standard room
Console.WriteLine("Booking for standard room is confirmed");
}
else
{
// pass the request to the next handler in the chain
nextHandler.HandleBooking(booking);
}
}
}
// Concrete handler for handling deluxe room bookings
class DeluxeRoomHandler : BookingHandler
{
public override void HandleBooking(Booking booking)
{
if (booking.Type == RoomType.Deluxe)
{
// handle booking for deluxe room
Console.WriteLine("Booking for deluxe room is confirmed");
}
else
{
// pass the request to the next handler in the chain
nextHandler.HandleBooking(booking);
}
}
}
// Concrete handler for handling luxury room bookings
class LuxuryRoomHandler : BookingHandler
{
public override void HandleBooking(Booking booking)
{
if (booking.Type == RoomType.Luxury)
{
// handle booking for luxury room
Console.WriteLine("Booking for luxury room is confirmed");
}
else
{
// handle the case where the booking type is not recognized
Console.WriteLine("Error: Unrecognized booking type");
}
}
}
// Booking class representing a room booking request
class Booking
{
public RoomType Type { get; set; }
}
// Enum representing the type of room
enum RoomType
{
Standard,
Deluxe,
Luxury
}
// Client code
class Program
{
static void Main(string[] args)
{
// create the chain of handlers
BookingHandler standardHandler = new StandardRoomHandler();
BookingHandler deluxeHandler = new DeluxeRoomHandler();
BookingHandler luxuryHandler = new LuxuryRoomHandler();
// set the order of the handlers in the chain
standardHandler.SetNext(deluxeHandler);
deluxeHandler.SetNext(luxuryHandler);
// create a booking request and pass it through the chain of handlers
Booking booking = new Booking();
booking.Type = RoomType.Standard;
standardHandler.HandleBooking(booking);
booking.Type = RoomType.Deluxe;
standardHandler.HandleBooking(booking);
booking.Type = RoomType.Luxury;
standardHandler.HandleBooking(booking);
Console.ReadKey();
}
}
What did the chain of responsibility object say to the handler that was executing the request? “I may be just a small object, but I have the power to pass the request along to the right place.”
The Chain of Responsibility design pattern is useful in situations where you want to decouple the sender of a request from its receiver. This allows for a more modular and flexible system, as the sender does not need to know which receiver can handle the request and the receiver can handle the request without knowing the sender.
Here are some examples of when you might use the Chain of Responsibility pattern:
1. In a web application, a user request is received by a controller which acts as the sender. The controller does not know which specific handler should handle the request, so it passes the request to a chain of handlers. Each handler in the chain checks if it can handle the request, and if not, passes it on to the next handler in the chain. The request is finally handled by the appropriate handler.
2. In a customer support system, a customer query is received by the system and passed to a chain of customer support agents. Each agent checks if they can handle the query, and if not, passes it on to the next agent in the chain. The query is finally handled by the appropriate agent.
3. In an e-commerce platform, a payment request is received by the system and passed to a chain of payment gateways. Each gateway checks if it can handle the request, and if not, passes it on to the next gateway in the chain. The request is finally handled by the appropriate gateway.
These are just a few examples of when you might use the Chain of Responsibility pattern. There are many other scenarios where this pattern can be useful.
Here is an example implementation of the Chain of Responsibility pattern for payment gateways using C#:
public abstract class PaymentGateway
{
protected PaymentGateway Successor;
public void SetSuccessor(PaymentGateway successor)
{
this.Successor = successor;
}
public abstract void ProcessPayment(double amount);
}
public class CreditCardPaymentGateway : PaymentGateway
{
public override void ProcessPayment(double amount)
{
if (amount <= 1000)
{
// Process payment with credit card.
Console.WriteLine("Payment of $" + amount + " processed with credit card.");
}
else
{
// Pass on to the next gateway in the chain.
this.Successor.ProcessPayment(amount);
}
}
}
public class BankTransferPaymentGateway : PaymentGateway
{
public override void ProcessPayment(double amount)
{
if (amount > 1000 && amount <= 10000)
{
// Process payment with bank transfer.
Console.WriteLine("Payment of $" + amount + " processed with bank transfer.");
}
else
{
// Pass on to the next gateway in the chain.
this.Successor.ProcessPayment(amount);
}
}
}
public class CashPaymentGateway : PaymentGateway
{
public override void ProcessPayment(double amount)
{
if (amount > 10000)
{
// Process payment with cash.
Console.WriteLine("Payment of $" + amount + " processed with cash.");
}
else
{
// This is the last gateway in the chain, so we throw an exception if we reach this point.
throw new Exception("Invalid payment amount: " + amount);
}
}
}
// Example usage:
var creditCard = new CreditCardPaymentGateway();
var bankTransfer = new BankTransferPaymentGateway();
var cash = new CashPaymentGateway();
// Set up the chain of responsibility:
creditCard.SetSuccessor(bankTransfer);
bankTransfer.SetSuccessor(cash);
// Process a payment:
creditCard.ProcessPayment(500); // Payment of $500 processed with credit card.
creditCard.ProcessPayment(2000); // Payment of $2000 processed with bank transfer.
creditCard.ProcessPayment(15000); // Payment of $15000 processed with cash.
In this case, the request is a payment of a certain amount, and the objects are payment gateway objects that handle payments of different amounts.
The PaymentGateway class is an abstract base class that defines the common functionality for all payment gateway objects. It has a single method, ProcessPayment, that is declared as abstract and must be implemented by derived classes. It also has a protected property called Successor, which represents the next object in the chain of responsibility that will handle the request if the current object is unable to do so.
The CreditCardPaymentGateway, BankTransferPaymentGateway, and CashPaymentGateway classes are derived from the PaymentGateway class and provide specific implementations of the ProcessPayment method for processing payments with a credit card, bank transfer, and cash, respectively.
Each of these classes checks if it can handle the payment based on the amount of the payment. If the payment amount is within the range that the class can handle, it processes the payment and outputs a message to the console. Otherwise, it passes the request on to the next object in the chain (the successor) by calling the ProcessPayment method on the successor object.
The CashPaymentGateway class is the last object in the chain, so if the payment amount is not within its range, it throws an exception.
In the example usage provided, three payment gateway objects are created and then set up in a chain of responsibility. The SetSuccessor method is called on each object to set the next object in the chain. Then, the ProcessPayment method is called on the first object in the chain to handle a payment of $500, $2000, and $15000. In each case, the request is passed along the chain until it is handled by the appropriate payment gateway object.
It looks like we can handle that with a few if condition. But that’s not the point.
What did the chain of responsibility object say to the client when it couldn’t handle the request? “I’m sorry, I’m just a small link in the chain. I’ll pass it along to someone else.”
UML Diagram
In this diagram, the AbstractHandler
class defines an interface for handling requests and a reference to its successor in the chain. The ConcreteHandler
classes handle requests they are responsible for and pass on unhandled requests to their successors. The Client
class sends requests to the first handler in the chain.
Conclusion
The point of using the chain of responsibility pattern is to decouple the sender of a request from its receiver. In other words, the code that sends a request (in this case, the code that calls the ProcessPayment method) does not need to know which object in the chain will handle the request. It only needs to know the first object in the chain.
This has several benefits. For one, it makes the code more flexible, because the objects in the chain can be rearranged or replaced without affecting the code that sends the requests. Additionally, it allows us to add new payment gateway objects to the chain without modifying the existing code.
Another benefit is that it allows each payment gateway object to handle only the payments that it is designed to handle. In the example code, each payment gateway class has a specific range of payment amounts that it can handle. If a payment is outside of that range, it passes the request on to the next object in the chain. This makes the code easier to understand and maintain, because each class has a clearly defined responsibility.
Using a chain of if conditions would not provide these benefits. The code would be more tightly coupled and less flexible, and it would be harder to add new payment gateway objects or rearrange the existing ones. Additionally, the code would be more complex, because all of the different payment gateway objects and their ranges would need to be checked in a single if statement.
Here is one more implementation sample of the Chain of Responbility design pattern on C#:
public abstract class Middleware
{
protected Middleware Successor;
public void SetSuccessor(Middleware successor)
{
this.Successor = successor;
}
public abstract Task InvokeAsync(HttpContext context);
}
public class AuthenticationMiddleware : Middleware
{
public override Task InvokeAsync(HttpContext context)
{
// Check if the request is authenticated.
if (!context.User.Identity.IsAuthenticated)
{
// Return a 401 Unauthorized response.
context.Response.StatusCode = 401;
return;
}
// Pass the request on to the next middleware in the chain.
return this.Successor.InvokeAsync(context);
}
}
public class LoggingMiddleware : Middleware
{
public override Task InvokeAsync(HttpContext context)
{
// Log the request.
Console.WriteLine("Received request: " + context.Request.Path);
// Pass the request on to the next middleware in the chain.
return this.Successor.InvokeAsync(context);
}
}
public class RequestHandlerMiddleware : Middleware
{
public override Task InvokeAsync(HttpContext context)
{
// This is the last middleware in the chain, so we handle the request here.
// Handle the request and generate a response.
// ...
return Task.CompletedTask;
}
}
// Example usage:
var authentication = new AuthenticationMiddleware();
var logging = new LoggingMiddleware();
var requestHandler = new RequestHandlerMiddleware();
// Set up the middleware chain:
authentication.SetSuccessor(logging);
logging.SetSuccessor(requestHandler);
// Use the middleware chain to handle a request:
await authentication.InvokeAsync(context);
Thanks for reading! If you found the article helpful, you can clap and follow. So you will notified of new articles.
# References
It was created with the help of ChatGPT AI.