Firstly, let's understand what Chain Of Responsibility Design Pattern is.
It is one of the Behavioral Design Patterns out of 23 patterns defined in the Gang Of Four Design Patterns. The usage of this pattern is not so prevalant and is categorized as 'Medium-Low' for the frequency of usage.
The pattern inherently implements 'Choreogrpahy' pattern of algorithm as opposed to Orchestration. To put it in an over-simplified context, if there are multiple objects handling a job, then there is no single super-object that controls the algorithm of who gets to solve the problem. Instead the start object A passes the batton to the next object B in line as it knows only about its own job and its successor, not who all are there in the game. Object B, if capable of doing the job, completes it and passes the signal back to the initiator. If not it passes the control to object C and so on.
In order to discuss the usage for the Chain-Of-Responsibility pattern, let's check a simple use case.
In this blog, I will not use interfaces and classes to demonstrate the use case. Instead will use multicast delegates to achieve the objective.
Before I proceed, let me put my disclaimer,
- The use of multicast delegate is just for illustration of the usage of delegates using the aforementioned design pattern.
- The delegates are not substitutes for interfaces and classes. In fact, delegates represent methods inside classes.
- Since the delegate can represent methods that have the same signature and return type, it's kind of analogous to enforcing Interface methods inside different concrete classes that inherits that interface. Hence the below-mentioned approach.
Notes on Delegates
There are multiple authors on this platform who have written in detail on delegates and explained it aptly. However, just for the sake of context, a brief note is presented here.
A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type. You can invoke (or call) the method through the delegate instance.
Syntax for delegate declaration is,
- delegate <return type> <delegate-name> <parameter list>
Multicasting of a Delegate
You can put multiple methods (matching the return and parameter list) to the same delegate instance. Using this property of delegates you can create an invocation list of methods that will be called when a delegate is invoked. This is called multicasting of a delegate.
With that as background, let's proceed on our example of -Implementing the Invoice approval process using multicast delegates that features the Chain-Of-Responsibility Design Pattern.
Step 1
Our Expense Invoice entity class,
- public class ExpenseInvoice
- {
- public decimal Amount { get; set; }
- public DateTime ExpenseDate { get; set; }
- public string Item { get; set; }
- public bool IsAmountApproved { get; set; }
- }
Step 2
Let's define our class that will do the processing and starts with defining the delegate. We have defined a delegate that returns nothing but takes the ExpenseInvoice class as input that we defined in Step 1. Now this delegate can reference and represent and the method has the same signature - returns void and intakes ExpenseInvoice type.
- public class MultiCastDelegateExample
- {
- private delegate void GetAmountApproved(ExpenseInvoice e);
- }
Step 3
Let me also define another generic delegate that would calculate the amount without the tax. This anonymous func will take a decimal value; calculate the actual expense without the tax (considered 10% of the amount in the invoice) and return the remaining amount as a decimal itself. We will use this delegate in our actual approval concrete methods.
- public class MultiCastDelegateExample
- {
- private delegate void GetAmountApproved(ExpenseInvoice e);
- private readonly Func<decimal, decimal> AmountWithoutTax = (a) => { return a * 0.9M; };
-
- }
Step 4
Now let's define our four different Approvers methods for the different thresholds of approval. Note that each of these methods has a matching signature of the delegate defined in Step 2 - returns void and accepts ExpenseInvoice type. Also, the amount value checked is the amount without the tax, for which we have used the second delegate defined (the anonymous function) in Step 3.
- public class MultiCastDelegateExample
- {
- private delegate void GetAmountApproved(ExpenseInvoice e);
- private readonly Func<decimal, decimal> AmountWithoutTax = (a) => { return a * 0.9M; };
-
- private void ApprovalByManager(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 1000;
- if (approved) Console.WriteLine("The Manager approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- private void ApprovalByDeliveryManager(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 10000;
- if (approved) Console.WriteLine("The Delivery Manager approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- private void ApprovalByVicePresident(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 100000;
- if (approved) Console.WriteLine("The Vice President approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- private void ApprovalByPresident(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 1000000;
- if (approved) Console.WriteLine("The President approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- }
Step 5
Now let's define the only public method of the class that would encapsulate the entire chain. We are using the delegate defined in Step 2 and assigned the first level of approver - the Manager.
- public void GetApproval(ExpenseInvoice ei)
- {
- GetAmountApproved approval = ApprovalByManager;
- approval.Invoke(ei);
- }
Step 6
Hereafter, the sequence in which you add other methods will determine the order in which the chain is executed. This is multicasting the delegate.
- public void GetApproval(ExpenseInvoice ei)
- {
-
- GetAmountApproved approval = ApprovalByManager;
- approval += ApprovalByDeliveryManager;
- approval += ApprovalByVicePresident;
- approval += ApprovalByPresident;
-
- approval.Invoke(ei);
- }
The finished class will look something like this,
- public class MultiCastDelegateExample
- {
- private delegate void GetAmountApproved(ExpenseInvoice e);
- private readonly Func<decimal, decimal> AmountWithoutTax = (a) => { return a * 0.9M; };
-
- public void GetApproval(ExpenseInvoice ei)
- {
-
- GetAmountApproved approval = ApprovalByManager;
- approval += ApprovalByDeliveryManager;
- approval += ApprovalByVicePresident;
- approval += ApprovalByPresident;
-
- approval.Invoke(ei);
- }
-
- private void ApprovalByManager(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 1000;
- if (approved) Console.WriteLine("The Manager approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- private void ApprovalByDeliveryManager(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 10000;
- if (approved) Console.WriteLine("The Delivery Manager approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- private void ApprovalByVicePresident(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 100000;
- if (approved) Console.WriteLine("The Vice President approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- private void ApprovalByPresident(ExpenseInvoice e)
- {
- if (!e.IsAmountApproved)
- {
- bool approved = AmountWithoutTax(e.Amount) < 1000000;
- if (approved) Console.WriteLine("The President approved the amount {0} without the tax (10%)", AmountWithoutTax(e.Amount));
- e.IsAmountApproved = approved;
- }
- }
-
- }
Step 7
Finally, the Main method (the client program) would initiate the approval process for a particular Expense Invoice. Here I have defined four different Expense Invoices at different amount levels which would make them flow into different levels progressively.
- static void Main(string[] args)
- {
-
-
- DelegateExamples de = new DelegateExamples();
- Domains.ExpenseInvoice ei1 = new Domains.ExpenseInvoice() { Amount = 1001.0M, ExpenseDate = Convert.ToDateTime("12-Mar-2021"), Item = "Stationary" };
- Domains.ExpenseInvoice ei2 = new Domains.ExpenseInvoice() { Amount = 10001.0M, ExpenseDate = Convert.ToDateTime("14-Mar-2021"), Item = "Computer HDD" };
- Domains.ExpenseInvoice ei3 = new Domains.ExpenseInvoice() { Amount = 100001.0M, ExpenseDate = Convert.ToDateTime("16-Mar-2021"), Item = "Hotel Room Rent" };
- Domains.ExpenseInvoice ei4 = new Domains.ExpenseInvoice() { Amount = 1000001.0M, ExpenseDate = Convert.ToDateTime("18-Mar-2021"), Item = "SUV" };
-
- Console.WriteLine("The Amount {0} is under approval process.", ei1.Amount.ToString());
- de.GetApproval(ei1);
- Console.WriteLine();
- Console.WriteLine("The Amount {0} is under approval process.", ei2.Amount.ToString());
- de.GetApproval(ei2);
- Console.WriteLine();
- Console.WriteLine("The Amount {0} is under approval process.", ei3.Amount.ToString());
- de.GetApproval(ei3);
- Console.WriteLine();
- Console.WriteLine("The Amount {0} is under approval process.", ei4.Amount.ToString());
- de.GetApproval(ei4);
- Console.WriteLine();
- Console.ReadLine();
- }
Step 8
Execute the program and your output should be something like.
- The Amount 1001.0 is under approval process.
- The Manager approved the amount 900.90 without the tax (10%)
-
- The Amount 10001.0 is under approval process.
- The Delivery Manager approved the amount 9000.90 without the tax (10%)
-
- The Amount 100001.0 is under approval process.
- The Vice President approved the amount 90000.90 without the tax (10%)
-
- The Amount 1000001.0 is under approval process.
- The President approved the amount 900000.90 without the tax (10%)
Conclusion
- Multicast Delegate allows you to create an invocation sequence.
- All methods part of the sequence must have the same signature - return type and the parameter list - type and count.
- Be aware of the parameter whether it's a value type or a reference type. The value type is passed at the root value to all methods in sequence. Even if the first method in the sequence changes the value of the input, it's not going to impact the second method in the sequence. The second method is going to process the root value only.
- You can make the method calls parallel by using async Tasks in the signature of the delegate. You have to implement additional logic to cumulate the response of all the tasks and then pass them to the calling client method.
Do let me know your feedback. Stay safe and stay blessed!