Understanding the Chain of Responsibility Pattern in C#

Search for a command to run...

No comments yet. Be the first to comment.
Design patterns solve common software design problems, enhancing quality and speed. They include Creational (e.g., Singleton), Structural (e.g., Adapter), and Behavioral (e.g., Observer) patterns for maintainable software.
In software development, as systems grow more complex, objects start interacting with each other in increasingly intricate ways. Over time, this can lead to a tightly coupled, hard-to-maintain system where modifying or extending a particular feature ...
Deploying a .NET application to a cloud environment like DigitalOcean is a powerful way to ensure scalability, reliability, and ease of management. In this post, we’ll walk through deploying a real-world .NET application—a simple ASP.NET Core MVC app...

Building a gRPC Service in C# and ASP.NET Core – Part 2

Building a gRPC Service in C# and ASP.NET Core – Part 1

Software architecture is the backbone of any application, and a well-thought-out architecture can make the difference between a maintainable, scalable application and a fragile, difficult-to-modify one. Clean Architecture, popularized by Robert C. Ma...

In software development, as systems grow more complex, objects start interacting with each other in increasingly intricate ways. Over time, this can lead to a tightly coupled, hard-to-maintain system where modifying or extending a particular feature ...

In software design, handling requests in a clean and flexible manner is often a challenge. The Chain of Responsibility (CoR) pattern offers an elegant solution by decoupling the sender of a request from its receiver, allowing multiple handlers to process the request without the sender needing to know who handles it. This results in a flexible and maintainable system that promotes loose coupling.
In this blog, we will dive into how the Chain of Responsibility pattern works, its advantages, and how it can be implemented in C#.
The Chain of Responsibility pattern is a behavioral design pattern where a request is passed through a series (or chain) of handlers. Each handler can either process the request or pass it to the next handler in the chain. This approach gives the system flexibility in processing requests and allows you to easily add or remove handlers without affecting the client code.
Decoupling: The sender of a request does not need to know which handler will process it.
Dynamic Behavior: Handlers can be chained dynamically at runtime.
Flexibility: New handlers can be added to the chain without modifying existing code.
Imagine a scenario in customer support where a user submits a complaint. The complaint first goes to the customer support agent. If the agent can resolve it, they handle the issue. If not, the complaint is escalated to the supervisor, and so on, until it reaches the right person to solve the issue. This is exactly how the Chain of Responsibility pattern works.
In the Chain of Responsibility pattern, the key components are:
Handler: An interface or abstract class that defines the method to handle a request and a reference to the next handler.
Concrete Handler: Classes that implement the handler and either process the request or forward it to the next handler in the chain.
Client: Initiates the request, which gets passed down the chain.
Here's a basic class diagram:
Client
|
Handler -> ConcreteHandler1 -> ConcreteHandler2 -> ConcreteHandler3
Let’s implement a simple chain that processes requests based on the type of log level: Info, Warning, and Error.
public abstract class Logger
{
protected Logger nextLogger;
// Set the next logger in the chain
public void SetNextLogger(Logger nextLogger)
{
this.nextLogger = nextLogger;
}
// The method that each concrete handler will implement
public void LogMessage(int level, string message)
{
if (this.CanHandleLog(level))
{
Write(message);
}
else if (nextLogger != null)
{
nextLogger.LogMessage(level, message);
}
}
protected abstract bool CanHandleLog(int level);
protected abstract void Write(string message);
}
We will create three different concrete loggers for different log levels: Info, Warning, and Error.
public class InfoLogger : Logger
{
protected override bool CanHandleLog(int level)
{
return level == 1; // Info level
}
protected override void Write(string message)
{
Console.WriteLine("Info: " + message);
}
}
public class WarningLogger : Logger
{
protected override bool CanHandleLog(int level)
{
return level == 2; // Warning level
}
protected override void Write(string message)
{
Console.WriteLine("Warning: " + message);
}
}
public class ErrorLogger : Logger
{
protected override bool CanHandleLog(int level)
{
return level == 3; // Error level
}
protected override void Write(string message)
{
Console.WriteLine("Error: " + message);
}
}
Now that we have our concrete loggers, let's chain them together. The ErrorLogger will handle errors, WarningLogger will handle warnings, and InfoLogger will handle informational messages.
public class LoggerChain
{
public static Logger GetChainOfLoggers()
{
Logger errorLogger = new ErrorLogger();
Logger warningLogger = new WarningLogger();
Logger infoLogger = new InfoLogger();
// Set up the chain: info -> warning -> error
infoLogger.SetNextLogger(warningLogger);
warningLogger.SetNextLogger(errorLogger);
return infoLogger; // Starting point of the chain
}
}
Now, the client can pass messages to the logger chain, and each logger will handle the request based on the level of severity.
class Program
{
static void Main(string[] args)
{
Logger loggerChain = LoggerChain.GetChainOfLoggers();
// Pass different messages to the chain with varying levels
loggerChain.LogMessage(1, "This is an information message.");
loggerChain.LogMessage(2, "This is a warning message.");
loggerChain.LogMessage(3, "This is an error message.");
}
}
Info: This is an information message.
Warning: This is a warning message.
Error: This is an error message.
Loose Coupling: The sender of the request does not need to know who will handle it. This allows the code to be easily extended without modifying the sender.
Flexibility: Handlers can be easily added or removed from the chain without impacting other parts of the system.
Single Responsibility Principle: Each handler has a single responsibility, making it easier to manage and test.
When you have multiple handlers that can process a request, and the exact handler is not known in advance.
When you want to decouple the sender of a request from the logic that handles it.
When you need to add or modify handlers dynamically without changing the client code.
The Chain of Responsibility pattern provides a flexible way to handle requests through a series of handlers. In C#, it's easy to implement using abstract classes and method overrides. The pattern enhances maintainability, promotes separation of concerns, and allows you to easily extend functionality without altering the core logic.
By understanding this pattern and applying it in the right contexts, you can create more robust and adaptable software systems.