Chain of Responsibility - Pattern Clarity #9
The Chain of Responsibility design pattern allows you to pass a request through a series of handlers (objects), where each one can decide whether to handle it or forward it to the next. This approach is particularly helpful when multiple objects might handle a request, but you don't want the request sender tightly coupled to a specific receiver.
❗ Problem
Suppose you're building a web application where each incoming request must pass a series of checks: authentication, authorization, rate-limiting, validation, and logging. Initially, you might code these in a single block:
if (Authenticate(request)) {
if (Authorize(request)) {
if (RateLimitCheck(request)) {
if (Validate(request)) {
LogRequest(request);
// ... proceed ...
}
}
}
}
This approach soon reveals flaws:
✅ Solution
The Chain of Responsibility pattern provides a neater way to handle sequential checks by linking handler objects in a chain. Each handler knows only about its successor, and when it can't handle (or partially handles) the request, it passes it on.
Key elements include:
This design spreads out the checks into separate, small classes that form a pipeline. It becomes simple to insert a new handler or reorder them, without modifying existing code.
📚 Use Cases
⚖️ Benefits & Drawbacks
Pros:
Cons:
Recommended by LinkedIn
❗️ Distributed or multiple processes: the chain isn't automatically shared across machines. You might need a message queue or pub-sub approach if you want to distribute handling steps among different services.
🔧 How to implement
By using this pattern, you can break large "if-else" blocks into a pipeline of smaller classes, each with a single responsibility.
🔄 Handling concurrency
When multiple threads use the same chain:
💻 Code Example
Below is a simplified C# example that processes expense requests via a chain of approvers:
namespace PatternClarity.BehavioralPatterns.ChainOfResponsibility;
class PurchaseRequest
{
public int Id { get; }
public double Amount { get; }
public string Description { get; }
public PurchaseRequest(int id, double amount, string description)
{
Id = id;
Amount = amount;
Description = description;
}
}
abstract class Approver
{
protected Approver next;
public Approver SetNext(Approver approver)
{
next = approver;
return approver;
}
public abstract void ProcessRequest(PurchaseRequest request);
}
class TeamLead : Approver
{
public override void ProcessRequest(PurchaseRequest request)
{
if (request.Amount < 1000)
Console.WriteLine($"TeamLead approved request #{request.Id}");
else if (next != null)
next.ProcessRequest(request);
}
}
class Manager : Approver
{
public override void ProcessRequest(PurchaseRequest request)
{
if (request.Amount < 10000)
Console.WriteLine($"Manager approved request #{request.Id}");
else if (next != null)
next.ProcessRequest(request);
}
}
class Director : Approver
{
public override void ProcessRequest(PurchaseRequest request)
{
if (request.Amount < 50000)
Console.WriteLine($"Director approved request #{request.Id}");
else
Console.WriteLine($"Request #{request.Id} requires special board approval!");
}
}
class Program
{
static void Main()
{
// Build the chain
Approver teamLead = new TeamLead();
Approver manager = new Manager();
Approver director = new Director();
teamLead.SetNext(manager).SetNext(director);
// Test requests
teamLead.ProcessRequest(new PurchaseRequest(1, 500, "Office Supplies"));
teamLead.ProcessRequest(new PurchaseRequest(2, 5000, "Workstations"));
teamLead.ProcessRequest(new PurchaseRequest(3, 30000, "New Servers"));
teamLead.ProcessRequest(new PurchaseRequest(4, 120000, "Data Center Upgrade"));
}
}
Console output:
TeamLead approved request #1
Manager approved request #2
Director approved request #3
Request #4 requires special board approval!
Here, each approver either handles the request or passes it on. You can easily insert new steps (e.g., VicePresident) by adjusting the chain construction.
Conclusion
Chain of Responsibility is a powerful way to organize multiple potential handlers for a request, preventing massive if-else blocks. By distributing tasks among small, testable handler classes, you enhance maintainability. But beware of too many handlers, tricky ordering, and potential debugging hurdles.
Sometimes you might use a framework's built-in middleware pipeline, or even a pub-sub approach for looser coupling in distributed systems. Choose what fits your needs best. When done right, CoR keeps your code flexible and clean!
Thank you for reading this article in the "Pattern Clarity" series! I'd love to hear your thoughts and experiences with the Chain of Responsibility pattern - share your comments and suggestions. Let's keep the conversation going!