Design Patterns: Chain of Responsibility Pattern

The Chain of Responsibility pattern is a simple pattern used in object-oriented designs. It is not as common as some of the other patterns, simply because it only applies in certain situations. However, when it does apply, it can simplify the design and keep it compliant with the SOLID design principles. The pattern applies when the design involves a series of request handlers that can either processes a specific request or pass it along to the next handler for further processing. In this design, the client interacts with a specific defined interface that is implemented by all the request handlers. The request handlers are then chained in a specific order. The Chain of Responsibility pattern can be thought of as an escalation process. The first handler attempts to process the request if it can. If not, it will escalate the request to the next logical handler. This design pattern shields the client from any knowledge of which handler ended up processing the request. It will simply receive a response without having to worry about the underlying mechanics of the system.

Example without Using the Chaim of Responsibility Pattern
The example below is a very popular one used to explain the Chain of Responsibility pattern. In this example, an employee wishes to generate an expense report and submit it for approval from their manager. The expense report contains a certain dollar amount representing the total of all the constituent changes making up the expense report. If the manager is unable to approve the report due to the amount exceeding their allowed maximum account, the manager will then forward the report up the company hierarchy until someone up the chain can approve the report.

Below is the definition of the expense report and the status Enum

ExpenseReport.cs

public class ExpenseReport
{
     public double Amount { get; set; }
     public ExpenseReportStatus Status { get; set; }

     public ExpenseReport(double amount)
     {
          Amount = amount;
          Status = ExpenseReportStatus.NotApproved;
     }
}

public enum ExpenseReportStatus
{
     Approved,
     NotApproved
}

The Employee class represents an individual that is capable of approving an expense report. Each employee has an approval limit value that they can use to approve the reports. Any report with a value below the approval limit can be approved by the employee.

Employee.cs

public class Employee
{
     public string Name { get; set; }
     public double ApprovalLimit { get; set; }

     public Employee(string name, double approvalLimit)
     {
          Name = name;
          ApprovalLimit = approvalLimit;
     }

     public void ApproveExpenseReport(ExpenseReport expenseReport)
     {
          if (expenseReport.Amount < ApprovalLimit)
          {
               expenseReport.Status = ExpenseReportStatus.Approved;
          }
     }
}

In the main function below, a series of employees are created with various names and approval limits. The employee Steve has the lowest approval limit of $200, followed by his manager Frank with an approval limit of $300 then by an executive Alice who has the highest approval limit of $500. An expense report with the amount of $320.00 is created and handed to the employee for approval. Since the amount $320 is above the employee's maximum limit, the program hands the expense report to the manager for approval. Since the manager's limit is $300, they will still not be able to approve the report, so the it is forwarded to the executive Alice who finally approves it. However, if the expense report amount exceeded Alice's maximum approval limit of $500, the expense report is never approved.

Program.cs and Main

public class Program
    {
        public static void Main(string[] args)
        {
            var employee = new Employee("Steve", 200);
            var manager = new Employee("Frank", 300);
            var executive = new Employee("Alice", 500);

            var expenseReport = new ExpenseReport(320.0);

            employee.ApproveExpenseReport(expenseReport);
            if (expenseReport.Status == ExpenseReportStatus.Approved)
            {
                Console.WriteLine(expenseReport.Status);
            }
            else
            {
                manager.ApproveExpenseReport(expenseReport);
                if (expenseReport.Status == ExpenseReportStatus.Approved)
                {
                    Console.WriteLine(expenseReport.Status);
                }
                else
                {
                    executive.ApproveExpenseReport(expenseReport);
                    if (expenseReport.Status == ExpenseReportStatus.Approved)
                    {
                        Console.WriteLine(expenseReport.Status);
                    }
                    else
                    {
                        Console.WriteLine(expenseReport.Status);
                    }
                }
            }
        }
    }

The code above reeks with code smell and it can be improved in many ways even without using the Chain of Responsibility pattern. The Employee class can implement a common interface and the giant 'if' statement can be replaced with a loop that iterates dynamically over a chain of employees. However, even by doing so, the client is not shielded from the knowledge of how the underlying system works. In other words, the single responsibility principle is being violated. The logic of escalating the expense report through a chain of employees need to be moved out of the main class into a common service whose sole responsibility is to approve the report through a series of employees. The main function should not be concerned with the details.

Example using the Chain of Responsibility Pattern
The first step for implementing the Chain of Responsibility pattern is to enhance the Employee class by adding the new property 'ReportsTo' that is also of type Employee. This property is use to reference the employee who is the manager of the current employee. The constructor of the employee class then accepts an instance of the manager to initialize the new field.

Next, the ApproveExpenseReport() function is enhanced to 'escalate' the report to the manager in case the current employee is unable to approve the expense report.

Employee.cs

public class Employee
{
     public string Name { get; set; }
     public double ApprovalLimit { get; set; }
     public Employee ReportsTo { get; set; }

     public Employee(string name, double approvalLimit, Employee reportsTo)
     {
          Name = name;
          ApprovalLimit = approvalLimit;
          ReportsTo = reportsTo;
     }

     public void ApproveExpenseReport(ExpenseReport expenseReport)
     {
          if (expenseReport.Amount < ApprovalLimit)
          {
               expenseReport.Status = ExpenseReportStatus.Approved;
          }

          if (ReportsTo != null)
          {
               ReportsTo.ApproveExpenseReport(expenseReport);
          }
     }
}

Finally, the main function is greatly simplified by removing the giant 'if' statement all together and replacing with the single call to the ApproveExpenseReport() function call on the employee object. The escalation process is handled entirely by the function shielding the client from knowing how to escalate the expense report.

Program.cs and Main

public class Program
{
     public static void Main(string[] args)
     {
          var executive = new Employee("Alice", 500, null);
          var manager = new Employee("Frank", 300, executive);
          var employee = new Employee("Steve", 200, manager);

          var expenseReport = new ExpenseReport(500.0);

          employee.ApproveExpenseReport(expenseReport);
          Console.WriteLine(expenseReport.Status);
     }
}

Further Improvements
The example above can by further improved by moving the ApproveExpenseReport() method into a separate service dedicated solely to approving expense report. This ensures the the Employee class is kept as a simple POCO. Also, the Employee class can implement a common interface, and a null-object implementation of the Employee class can be used as a terminating condition in the case where an employee is an executive and has no manager.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *