Understanding the Chain of Responsibility Design Pattern in C#

The Chain of Responsibility design pattern is a behavioral pattern that allows an object to pass a request along a chain of potential handlers until one of them handles the request. This pattern decouples senders and receivers of requests.
Example without Chain of Responsibility Design Pattern
Let's consider a scenario where different levels of support are provided in a customer service system: Basic Support, Technical Support, and Manager Support. Each level can handle different types of customer inquiries based on their complexity. using System; namespace WithoutChainOfResponsibilityPattern { // Request class class CustomerServiceRequest { public string RequestType { get; set; } public CustomerServiceRequest(string requestType) { RequestType = requestType; } } // Concrete class for handling requests class CustomerService { public void HandleRequest(CustomerServiceRequest request) { if (request.RequestType == "Simple") { Console.WriteLine("Basic Support can handle simple requests."); } else if (request.RequestType == "Complex") { Console.WriteLine("Technical Support can handle complex requests."); } else if (request.RequestType == "Escalated") { Console.WriteLine("Manager Support can handle escalated requests."); } else { Console.WriteLine("No handler available for this request."); } } } class Program { static void Main(string[] args) { CustomerService customerService = new CustomerService(); // Process requests CustomerServiceRequest request1 = new CustomerServiceRequest("Simple"); customerService.HandleRequest(request1); CustomerServiceRequest request2 = new CustomerServiceRequest("Complex"); customerService.HandleRequest(request2); CustomerServiceRequest request3 = new CustomerServiceRequest("Escalated"); customerService.HandleRequest(request3); CustomerServiceRequest request4 = new CustomerServiceRequest("Unknown"); customerService.HandleRequest(request4); } } }
Problems in the Non-Pattern Approach
Tight Coupling:The
CustomerService
class is tightly coupled to the logic for handling different types of requests. Any change in the handling logic requires modifying theCustomerService
class.Scalability Issues:The
CustomerService
class can become bloated as it needs to handle more request types. Adding new request types increases the complexity of theCustomerService
class.Single Responsibility Principle Violation:The
CustomerService
class is responsible for both handling requests and determining which handler should process the request. This violates the Single Responsibility Principle.Lack of Flexibility:Adding or modifying the request handling logic is not flexible. It requires changes in the main
CustomerService
class, which can lead to more errors and makes the code harder to maintain.
How the Chain of Responsibility Pattern Solves These Problems
Loose Coupling:The Chain of Responsibility Pattern decouples the sender of a request from its receivers. Each handler in the chain only knows how to handle specific request types and passes other requests down the chain.
Scalability:New handlers can be added easily without modifying existing code. This makes the system more scalable and the
CustomerService
class remains clean and maintainable.Single Responsibility Principle:Each handler class has a single responsibility: handling its specific request type. This adheres to the Single Responsibility Principle and makes the code easier to maintain.
Flexibility:The Chain of Responsibility Pattern provides flexibility in changing the order of handlers or adding new handlers. Each handler operates independently, making the system more adaptable to changes.
Revisited Code with Chain of Responsibility Pattern
Here is how we can implement this pattern :
using System;
namespace ChainOfResponsibilityPattern
{
// Request class
class CustomerServiceRequest
{
public string RequestType { get; set; }
public CustomerServiceRequest(string requestType)
{
RequestType = requestType;
}
}
// Handler interface
interface ICustomerServiceHandler
{
void HandleRequest(CustomerServiceRequest request);
void SetNextHandler(ICustomerServiceHandler nextHandler);
}
// Concrete handler for Basic Support
class BasicSupportHandler : ICustomerServiceHandler
{
private ICustomerServiceHandler _nextHandler;
public void SetNextHandler(ICustomerServiceHandler nextHandler)
{
_nextHandler = nextHandler;
}
public void HandleRequest(CustomerServiceRequest request)
{
if (request.RequestType == "Simple")
{
Console.WriteLine("Basic Support can handle simple requests.");
}
else if (_nextHandler != null)
{
_nextHandler.HandleRequest(request);
}
else
{
Console.WriteLine("No handler available for this request.");
}
}
}
// Concrete handler for Technical Support
class TechnicalSupportHandler : ICustomerServiceHandler
{
private ICustomerServiceHandler _nextHandler;
public void SetNextHandler(ICustomerServiceHandler nextHandler)
{
_nextHandler = nextHandler;
}
public void HandleRequest(CustomerServiceRequest request)
{
if (request.RequestType == "Complex")
{
Console.WriteLine("Technical Support can handle complex requests.");
}
else if (_nextHandler != null)
{
_nextHandler.HandleRequest(request);
}
else
{
Console.WriteLine("No handler available for this request.");
}
}
}
// Concrete handler for Manager Support
class ManagerSupportHandler : ICustomerServiceHandler
{
private ICustomerServiceHandler _nextHandler;
public void SetNextHandler(ICustomerServiceHandler nextHandler)
{
_nextHandler = nextHandler;
}
public void HandleRequest(CustomerServiceRequest request)
{
if (request.RequestType == "Escalated")
{
Console.WriteLine("Manager Support can handle escalated requests.");
}
else if (_nextHandler != null)
{
_nextHandler.HandleRequest(request);
}
else
{
Console.WriteLine("No handler available for this request.");
}
}
}
class Program
{
static void Main(string[] args)
{
// Create handlers
ICustomerServiceHandler basicHandler = new BasicSupportHandler();
ICustomerServiceHandler technicalHandler = new TechnicalSupportHandler();
ICustomerServiceHandler managerHandler = new ManagerSupportHandler();
// Set the chain of responsibility
basicHandler.SetNextHandler(technicalHandler);
technicalHandler.SetNextHandler(managerHandler);
// Process requests
CustomerServiceRequest request1 = new CustomerServiceRequest("Simple");
basicHandler.HandleRequest(request1);
CustomerServiceRequest request2 = new CustomerServiceRequest("Complex");
basicHandler.HandleRequest(request2);
CustomerServiceRequest request3 = new CustomerServiceRequest("Escalated");
basicHandler.HandleRequest(request3);
CustomerServiceRequest request4 = new CustomerServiceRequest("Unknown");
basicHandler.HandleRequest(request4);
}
}
}
Why Can't We Use Other Design Patterns Instead?
Strategy Pattern: The Strategy pattern defines a family of interchangeable algorithms and allows the client to choose which algorithm to use. It does not involve passing a request through a chain of handlers.
Template Method Pattern: The Template Method pattern defines the structure of an algorithm but allows subclasses to override certain steps. It is not designed for passing a request through multiple handlers.
Observer Pattern: The Observer pattern defines a one-to-many dependency between objects, where one object (subject) notifies its observers of any state changes. It is not related to handling requests through a chain of responsibility.
Steps to Identify Use Cases for the Chain of Responsibility Pattern
Multiple Handlers: Identify scenarios where multiple objects can handle a request, and the handler is not known at compile-time.
Dynamic Handling Order: When the order of handling objects can change dynamically based on runtime conditions or configuration.
Avoiding Coupling: When you want to avoid tightly coupling the sender of a request with its receivers, allowing for flexible handling chains.
By following these steps and implementing the Chain of Responsibility pattern, you can achieve decoupling between senders and receivers of requests, allowing for flexible and dynamic handling of requests through a chain of potential handlers.