Understanding and Implementing Chain of Responsibility Pattern in C#

Understanding and Implementing Chain of Responsibility Pattern in C#

Introduction

This article discusses the Chain of Responsibility design pattern. We will explore scenarios where this pattern is beneficial, its advantages, and provide a basic implementation in C#.

Background

In the Observer pattern, one class observes the state changes of another class. Observing classes register themselves to receive notifications when state changes occur, allowing them to act accordingly.

Building upon this concept, imagine the observing class takes actions based on certain conditions. If the condition isn’t met, it forwards the event to another object in line. In this case, a sequence of objects works together, where each object decides to either handle the request or pass it along the chain.

The Chain of Responsibility pattern is perfect for such scenarios. Here, an object listens for an event, handles it if possible, or delegates it to the next object. This pattern is particularly useful for implementing workflows.

The Gang of Four (GoF) defines this pattern as: "Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it."

Class Diagram Overview

To understand the Chain of Responsibility, let’s examine its components:

  1. Handler: An interface or abstract class implemented by all event-handling classes.
  2. ConcreteHandler: A class capable of handling events or forwarding them further.
  3. Client: Sets up the chain of successors and initiates requests to a handler.

Using the Code

To illustrate the pattern, let’s build a workflow for leave approvals in an organization:

  • Team Leader: Approves leaves for less than 10 days.
  • Project Leader: Approves leaves between 10 and 20 days.
  • HR: Approves leaves up to 30 days. Requests exceeding 30 days require manual processing.

We’ll implement the pattern with the following:

  1. Abstract Handler (Employee): Contains common functionality like tracking successors and initiating requests.
  2. Concrete Handlers: Define specific leave approval logic for Team Leader, Project Leader, and HR.

Abstract Handler: Employee

The Employee class is responsible for:

  • Tracking successors in the chain.
  • Managing events to notify and propagate requests.
  • Initiating requests.

Here’s the implementation:

public abstract class Employee
{
    // Every employee will have a supervisor
    protected Employee supervisor;

    // Event mechanism to handle leave applications
    public delegate void OnLeaveApplied(Employee e, Leave l);
    public event OnLeaveApplied onLeaveApplied;

    public void LeaveApplied(Employee sender, Leave leave)
    {
        onLeaveApplied?.Invoke(this, leave);
    }

    public abstract void ApproveLeave(Leave leave);

    public Employee Supervisor
    {
        get => supervisor;
        set => supervisor = value;
    }

    public void ApplyLeave(Leave leave)
    {
        LeaveApplied(this, leave);
    }
}        

Concrete Handlers

Team Leader

The TeamLeader class approves leaves under 10 days or forwards them to the next handler:

public class TeamLeader : Employee
{
    const int MAX_LEAVES_CAN_APPROVE = 10;

    public TeamLeader()
    {
        this.onLeaveApplied += TeamLeader_onLeaveApplied;
    }

    void TeamLeader_onLeaveApplied(Employee sender, Leave leave)
    {
        if (leave.NumberOfDays <= MAX_LEAVES_CAN_APPROVE)
        {
            ApproveLeave(leave);
        }
        else
        {
            Supervisor?.LeaveApplied(this, leave);
        }
    }

    public override void ApproveLeave(Leave leave)
    {
        Console.WriteLine($"LeaveID: {leave.LeaveID}, Days: {leave.NumberOfDays}, Approver: Team Leader");
    }
}        

Project Leader

The ProjectLeader handles leaves under 20 days and forwards others:

public class ProjectLeader : Employee
{
    const int MAX_LEAVES_CAN_APPROVE = 20;

    public ProjectLeader()
    {
        this.onLeaveApplied += ProjectLeader_onLeaveApplied;
    }

    void ProjectLeader_onLeaveApplied(Employee sender, Leave leave)
    {
        if (leave.NumberOfDays <= MAX_LEAVES_CAN_APPROVE)
        {
            ApproveLeave(leave);
        }
        else
        {
            Supervisor?.LeaveApplied(this, leave);
        }
    }

    public override void ApproveLeave(Leave leave)
    {
        Console.WriteLine($"LeaveID: {leave.LeaveID}, Days: {leave.NumberOfDays}, Approver: Project Leader");
    }
}        

HR

The HR class handles leaves up to 30 days and notifies users for manual approvals if needed:

public class HR : Employee
{
    const int MAX_LEAVES_CAN_APPROVE = 30;

    public HR()
    {
        this.onLeaveApplied += HR_onLeaveApplied;
    }

    void HR_onLeaveApplied(Employee sender, Leave leave)
    {
        if (leave.NumberOfDays <= MAX_LEAVES_CAN_APPROVE)
        {
            ApproveLeave(leave);
        }
        else
        {
            Console.WriteLine("Leave application suspended, please contact HR.");
        }
    }

    public override void ApproveLeave(Leave leave)
    {
        Console.WriteLine($"LeaveID: {leave.LeaveID}, Days: {leave.NumberOfDays}, Approver: HR");
    }
}        

Leave Class

Encapsulates leave details for processing:

public class Leave
{
    public Leave(Guid id, int days)
    {
        LeaveID = id;
        NumberOfDays = days;
    }

    public Guid LeaveID { get; set; }
    public int NumberOfDays { get; set; }
}        

Client Code

Finally, the Program class creates the chain and initiates requests:

class Program
{
    static void Main(string[] args)
    {
        TeamLeader tl = new TeamLeader();
        ProjectLeader pl = new ProjectLeader();
        HR hr = new HR();

        tl.Supervisor = pl;
        pl.Supervisor = hr;

        Leave leaveRequest = new Leave(Guid.NewGuid(), 25);
        tl.ApplyLeave(leaveRequest);
    }
}        

So we can see that this Main function is creating the chain by setting the successors for each class and is initiating the request to TeamLeader class. One request for each scenario has been made. When we run this application.

So we can see that each request get passed on and processed by the respective class based on the number of days applied for. Before wrapping up let us look at the class diagram for our application and compare it with the original GoF diagram.?

Conclusion

The Chain of Responsibility pattern decouples the sender and receiver, enabling multiple objects to handle requests in a defined sequence. It’s particularly useful for workflows and approval systems.

Try this pattern in your next project and share your experience!

#DesignPatterns #CSharpDevelopment #ChainOfResponsibility #SoftwareArchitecture

要查看或添加评论,请登录

Rahul Singh的更多文章

社区洞察

其他会员也浏览了