Understanding the Chain of Responsibility Pattern in C#

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#.

What is the Chain of Responsibility Pattern?

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.

Key Features:

  • 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.

Real-World Analogy

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.

Structure of the Chain of Responsibility Pattern

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

Implementing the Chain of Responsibility in C

Let’s implement a simple chain that processes requests based on the type of log level: Info, Warning, and Error.

Step 1: Define the Handler Interface

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);
}

Step 2: Create Concrete Handlers

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);
    }
}

Step 3: Build the Chain of Responsibility

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
    }
}

Step 4: Using 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.");
    }
}

Output:

Info: This is an information message.
Warning: This is a warning message.
Error: This is an error message.

Advantages of the Chain of Responsibility Pattern

  1. 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.

  2. Flexibility: Handlers can be easily added or removed from the chain without impacting other parts of the system.

  3. Single Responsibility Principle: Each handler has a single responsibility, making it easier to manage and test.

When to Use the Chain of Responsibility Pattern

  • 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.

Conclusion

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.