Better Use Case for Chain of Responsibility Pattern: Is It Just a Replacement for Multiple If-Else Statements?
Unraveling the Power of Chain of Responsibility Beyond Simple Conditional Logic
The Chain of Responsibility (CoR) pattern is a behavioral design pattern that delegates tasks along a chain of handlers. At first glance, it may seem like a fancy alternative to multiple if-else statements, but its real power lies in its ability to decouple request senders from receivers and create flexible chains of operations.
In this blog, we’ll explore:
- What the Chain of Responsibility pattern is.
- A comparison with multiple if-else statements.
- The scenarios where CoR truly shines.
- A real-world implementation in .NET Core.
📌Explore more at: https://dotnet-fullstack-dev.blogspot.com/
🌟 Clapping would be appreciated! 🚀
What Is the Chain of Responsibility Pattern?
The Chain of Responsibility pattern creates a chain of objects where each object decides whether to handle a request or pass it to the next object in the chain. This provides:
- Decoupling: The sender does not need to know who will handle the request.
- Extensibility: Adding or removing handlers does not impact other parts of the chain.
How It Works
- A client sends a request.
- Each handler in the chain either processes the request or forwards it to the next handler.
- The chain ends when a handler processes the request or no handler is left.
Comparison: Chain of Responsibility vs Multiple If-Else
Multiple If-Else Statements
In a straightforward scenario, you might use multiple if-else statements to handle conditions.
Example:
public class DiscountHandler
{
public string ApplyDiscount(decimal amount)
{
if (amount > 1000)
{
return "10% discount applied.";
}
else if (amount > 500)
{
return "5% discount applied.";
}
else
{
return "No discount.";
}
}
}
Drawbacks:
- Tightly Coupled Logic: Hard to add, remove, or reorder conditions.
- Rigid Code: Scaling becomes difficult as new conditions are added.
- Single Responsibility Violation: Mixing logic for condition checking and action processing.
Chain of Responsibility Pattern
The Chain of Responsibility solves these issues by separating conditions into discrete handlers.
Example:
public abstract class DiscountHandler
{
protected DiscountHandler _nextHandler;
public void SetNext(DiscountHandler nextHandler)
{
_nextHandler = nextHandler;
}
public abstract string Handle(decimal amount);
}
public class HighDiscountHandler : DiscountHandler
{
public override string Handle(decimal amount)
{
if (amount > 1000)
{
return "10% discount applied.";
}
else if (_nextHandler != null)
{
return _nextHandler.Handle(amount);
}
return "No discount.";
}
}
public class MediumDiscountHandler : DiscountHandler
{
public override string Handle(decimal amount)
{
if (amount > 500)
{
return "5% discount applied.";
}
else if (_nextHandler != null)
{
return _nextHandler.Handle(amount);
}
return "No discount.";
}
}
Benefits of CoR Over If-Else
When to Use Chain of Responsibility
While CoR can replace if-else statements, its real power lies in more complex scenarios:
1. Request Processing Pipelines
Example: A logging framework where logs pass through different handlers (e.g., console, file, or remote server).
2. Validation Chains
Example: Validating user input where each handler validates a specific rule (e.g., email, phone number, password strength).
3. Conditional Workflows
Example: An e-commerce checkout process with handlers for discount application, tax calculation, and payment processing.
Real-World Example in .NET Core
Scenario: Logging Framework
We’ll create a logging framework that processes log messages based on their severity.
Step 1: Define the Base Handler
public abstract class LogHandler
{
protected LogHandler _nextHandler;
public void SetNext(LogHandler nextHandler)
{
_nextHandler = nextHandler;
}
public abstract void Handle(string message, LogLevel level);
}
public enum LogLevel
{
Info,
Warning,
Error
}
Step 2: Implement Handlers
public class ConsoleLogHandler : LogHandler
{
public override void Handle(string message, LogLevel level)
{
if (level == LogLevel.Info)
{
Console.WriteLine($"Console Log: {message}");
}
else if (_nextHandler != null)
{
_nextHandler.Handle(message, level);
}
}
}
public class FileLogHandler : LogHandler
{
public override void Handle(string message, LogLevel level)
{
if (level == LogLevel.Warning)
{
Console.WriteLine($"File Log: {message}");
// Simulate writing to a file
}
else if (_nextHandler != null)
{
_nextHandler.Handle(message, level);
}
}
}
public class ErrorLogHandler : LogHandler
{
public override void Handle(string message, LogLevel level)
{
if (level == LogLevel.Error)
{
Console.WriteLine($"Error Log: {message}");
}
else if (_nextHandler != null)
{
_nextHandler.Handle(message, level);
}
}
}
Step 3: Create the Chain
public class Program
{
static void Main()
{
var consoleHandler = new ConsoleLogHandler();
var fileHandler = new FileLogHandler();
var errorHandler = new ErrorLogHandler();
consoleHandler.SetNext(fileHandler);
fileHandler.SetNext(errorHandler);
// Start the chain
consoleHandler.Handle("This is an info message.", LogLevel.Info);
consoleHandler.Handle("This is a warning message.", LogLevel.Warning);
consoleHandler.Handle("This is an error message.", LogLevel.Error);
}
}
Output:
Console Log: This is an info message.
File Log: This is a warning message.
Error Log: This is an error message.
Conclusion
While the Chain of Responsibility pattern can replace multiple if-else statements, it offers much more:
- It simplifies complex workflows.
- Makes code scalable and maintainable.
- Enhances readability and modularity.
For simple scenarios, if-else might suffice. However, for dynamic, extensible, and decoupled solutions, the Chain of Responsibility pattern is your go-to approach.
Which use case have you used Chain of Responsibility for? Share your thoughts in the comments below!