Passing the Baton: A Comprehensive Guide to the Chain of Responsibility Pattern in C#

The Chain of Responsibility Pattern is a behavioral design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or pass it to the next handler in the chain. This pattern promotes the decoupling of senders and receivers of requests and allows multiple objects to handle a request without the sender needing to know which object will ultimately process it. In this article, we will explore the Chain of Responsibility Pattern in-depth, examining its structure, advantages, and providing practical examples in C#.

Understanding the Chain of Responsibility Pattern

The Chain of Responsibility Pattern involves the following key components:

  1. Handler: The interface or abstract class that declares a method for handling requests. It usually contains a reference to the next handler in the chain.
  2. ConcreteHandler: The class that implements the Handler interface. It decides whether to handle the request or pass it to the next handler in the chain.
  3. Client: The class that initiates requests to the chain of handlers. It is unaware of which handler will process the request.

Implementation in C#

Let's delve into a simple example of the Chain of Responsibility Pattern in C#. Suppose we have a purchase approval system where purchase requests need to go through a chain of approvers based on the amount of the request. The Chain of Responsibility Pattern can be applied to create a chain of approvers.

// Step 1: Define Handler interface
public interface IApprover
{
    void ProcessRequest(PurchaseRequest request);
}

// Step 2: Implement ConcreteHandlers
public class Clerk : IApprover
{
    private readonly IApprover successor;

    public Clerk(IApprover successor)
    {
        this.successor = successor;
    }

    public void ProcessRequest(PurchaseRequest request)
    {
        if (request.Amount <= 100)
        {
            Console.WriteLine($"Clerk approves purchase request #{request.RequestNumber}");
        }
        else
        {
            successor?.ProcessRequest(request);
        }
    }
}

public class Manager : IApprover
{
    private readonly IApprover successor;

    public Manager(IApprover successor)
    {
        this.successor = successor;
    }

    public void ProcessRequest(PurchaseRequest request)
    {
        if (request.Amount > 100 && request.Amount <= 1000)
        {
            Console.WriteLine($"Manager approves purchase request #{request.RequestNumber}");
        }
        else
        {
            successor?.ProcessRequest(request);
        }
    }
}

public class Director : IApprover
{
    public void ProcessRequest(PurchaseRequest request)
    {
        if (request.Amount > 1000)
        {
            Console.WriteLine($"Director approves purchase request #{request.RequestNumber}");
        }
    }
}

// Step 3: Implement PurchaseRequest class
public class PurchaseRequest
{
    public int RequestNumber { get; }
    public double Amount { get; }

    public PurchaseRequest(int requestNumber, double amount)
    {
        RequestNumber = requestNumber;
        Amount = amount;
    }
}

// Step 4: Implement Client
public class Client
{
    private readonly IApprover approverChain;

    public Client(IApprover approverChain)
    {
        this.approverChain = approverChain;
    }

    public void ProcessPurchaseRequest(PurchaseRequest request)
    {
        approverChain.ProcessRequest(request);
    }
}

In this example, IApprover is the handler interface that declares the method for processing purchase requests. Clerk, Manager, and Director are concrete handlers that implement the IApprover interface. Each handler decides whether to approve the request or pass it to the next handler in the chain. The Client class initiates purchase requests and processes them through the chain of approvers.

Advantages of the Chain of Responsibility Pattern

1. Decoupling of Sender and Receiver: The pattern decouples the sender of a request from its receivers, allowing multiple objects to handle the request without the sender needing to know the exact processing logic.

2. Dynamic Handling: Handlers can be added or removed from the chain dynamically, providing flexibility in the processing order.

3. Promotes Single Responsibility Principle: Each handler has a single responsibility – either processing the request or passing it to the next handler.

4. Enhanced Maintainability: The pattern simplifies the maintenance of the code as changes in the chain do not affect the client or other handlers.

Real-world Examples

1. Workflow Processing

In a workflow processing system, the Chain of Responsibility Pattern can be applied to represent a series of processing steps. Each step in the workflow is a handler that either processes the data or passes it to the next step in the chain.

// Simplified example in C#
public interface IWorkflowStep
{
    void ProcessData(DataContext data);
}

public class DataValidationStep : IWorkflowStep
{
    private readonly IWorkflowStep successor;

    public DataValidationStep(IWorkflowStep successor)
    {
        this.successor = successor;
    }

    public void ProcessData(DataContext data)
    {
        // Validate data
        Console.WriteLine("Data validation completed.");
        successor?.ProcessData(data);
    }
}

public class DataTransformationStep : IWorkflowStep
{
    private readonly IWorkflowStep successor;

    public DataTransformationStep(IWorkflowStep successor)
    {
        this.successor = successor;
    }

    public void ProcessData(DataContext data)
    {
        // Transform data
        Console.WriteLine("Data transformation completed.");
        successor?.ProcessData(data);
    }
}

public class DataExportStep : IWorkflowStep
{
    public void ProcessData(DataContext data)
    {
        // Export data
        Console.WriteLine("Data export completed.");
    }
}

public class DataContext
{
    public string Data { get; set; }
}

public class WorkflowProcessor
{
    private readonly IWorkflowStep workflow;

    public WorkflowProcessor(IWorkflowStep workflow)
    {
        this.workflow = workflow;
    }

    public void ProcessWorkflow(DataContext data)
    {
        workflow.ProcessData(data);
    }
}

2. Event Handling in User Interfaces

In graphical user interfaces, the Chain of Responsibility Pattern can be used for handling user interface events. Each UI component (button, textbox, etc.) can be a handler that either handles the event or passes it to the next handler in the chain.

// Simplified example in C#
public interface IUIComponent
{
    void HandleEvent(UIEvent e);
}

public class Button : IUIComponent
{
    private readonly IUIComponent successor;

    public Button(IUIComponent successor)
    {
        this.successor = successor;
    }

    public void HandleEvent(UIEvent e)
    {
        // Handle button-specific events
        Console.WriteLine("Button handling event.");
        successor?.HandleEvent(e);
    }
}

public class TextBox : IUIComponent
{
    public void HandleEvent(UIEvent e)
    {
        // Handle textbox-specific events
        Console.WriteLine("TextBox handling event.");
    }
}

public class UIEvent
{
    public string Type { get; set; }
    public string Data { get; set; }
}

public class UIManager
{
    private readonly IUIComponent uiComponents;

    public UIManager(IUIComponent uiComponents)
    {
        this.uiComponents = uiComponents;
    }

    public void HandleUIEvent(UIEvent e)
    {
        uiComponents.HandleEvent(e);
    }
}

Conclusion

The Chain of Responsibility Pattern is a valuable tool for handling requests in a flexible and decoupled manner. Through practical examples in C#, we have demonstrated how the Chain of Responsibility Pattern can be applied to real-world scenarios, providing a blueprint for creating systems that involve a series of processing steps or event handling. Understanding and incorporating this pattern into your design practices can contribute to building modular, scalable, and maintainable software architectures, ensuring a smooth and dynamic flow of requests in your applications.