Last time when we discussed delegates, we created a small POC program to demonstrate what they are and how to work with them. You can find that mentioned material here. Now that the code is in place and working, it is time to write it well. Yes, yes, it is refactoring time.
This refactoring process aims to make the code more readable and more flexible for future notifications. I know, it sounds super cliché. To be more precise: I want to use some predefined code structures and show you the possible extension points when working with code written in such a way.
If you want to download the code, it can be found here.
Action and Func?
Declaring your own delegates is cool, I know. But there are two types built on top of the delegates that can be super handy in 99,99% of the situations. FYI, I just made that number up, so don't quote me on that.
Those two predefined generic types are Action and Func. A detailed description of these two types can be found here and here respectively. I strongly encourage you to get familiar with the content of these two documents. If, for some reason, you choose not to, here is a short recap: Action gives us a delegate with up to 16 input parameters and returns void., but on the other hand, Func, while also taking up to 16 input parameters, must return not-void.
Now let's see how the main class in our solution, MakeChanges, has changed during the refactoring process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
namespace Delegates; public class MakeChanges { // public delegate void ChangeValidated(); // public delegate bool Processing(ChangeDetails changeDetails); // public delegate void ProcessCompleted(ChangeDetails changeDetails); public Action OnChangeValidated { get; set; } public Func<ChangeDetails, bool> OnProcessing { get; set; } private bool Validate(ChangeDetails changeDetails) { try { ArgumentNullException.ThrowIfNull(changeDetails); } catch { return false; } OnChangeValidated?.Invoke(); return true; } public void Process(ChangeDetails changeDetails, Action<ChangeDetails>? processCompleted = default) { if (!Validate(changeDetails)) return; if (OnProcessing?.Invoke(changeDetails) == true) { processCompleted?.Invoke(changeDetails); } } }
At the very top of the code example, I have left the commented delegates' definitions. This is just for the sake of showing what we have removed. As you can see, we have removed all three delegates' definitions.
First, there are two delegates that got used as public properties. To make it more readable, I have placed the replaced commented-out delegates definition above the properties. Now they look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// public delegate void ChangeValidated(); public Action OnChangeValidated { get; set; } // public delegate bool Processing(ChangeDetails changeDetails); public Func<ChangeDetails, bool> OnProcessing { get; set; }
We can see that the ChangeValidated delegate was replaced with parameterless Action. This is because the delegate itself doesn’t return anything and take in input parameters. In the second property, we are based on a delegate that takes one input parameter and returns a bool value. Therefore, we are replacing that delegate with a Func<ChangeDetails, bool> - this declaration means that the delegate beneath expects an input parameter of type ChangeDetails and will return a bool. Easy. Isn’t it?
Not to the third change made in this file.
1 2 3 4 5 6 7 8 9 10
// public delegate void ProcessCompleted(ChangeDetails changeDetails); public void Process(ChangeDetails changeDetails, Action<ChangeDetails>? processCompleted = default) { if (!Validate(changeDetails)) return; if (OnProcessing?.Invoke(changeDetails) == true) { processCompleted?.Invoke(changeDetails); } }
In this case, we need to replace the delegate that takes an input of ChangeDetails and returns void. This means we need to use a generic Action with one parameter. Easy.
After the changes, the code looks much cleaner. The best part is that the outside of the class won’t even notice the change - we can still use the same code we were using before, and it will run just fine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
var change = new ChangeDetails() { Id = 1, Name = "Name 1", Valid = true }; var changer = new MakeChanges(); var changeUtils = new ChangeUtils(); changer.OnChangeValidated += changeUtils.SaveChangeToList; changer.OnChangeValidated += changeUtils.PrioritizeChange; changer.OnProcessing += changeUtils.Process; // OPTION 1 Action<ChangeDetails> processCompletedChain = changeUtils.DoPaperWork; processCompletedChain += changeUtils.CheckPaperWork; processCompletedChain += changeUtils.CleanUpAfterPaperWork; changer.Process(change, processCompletedChain);
The chain
The keen eye has surely noticed that there was one significant change in the code above. We are no longer the passing method to a callback parameter, as the method was changed to delegate. Well, to an Action, to be honest.
1 2 3 4 5 6
// OPTION 1 Action<ChangeDetails> processCompletedChain = changeUtils.DoPaperWork; processCompletedChain += changeUtils.CheckPaperWork; processCompletedChain += changeUtils.CleanUpAfterPaperWork; changer.Process(change, processCompletedChain);
This may not look like much, but this is huge, actually. Thanks to that, we can pass an entire chain of methods as a callback. That helps both with code organization and flexibility. While we are on the subject of code organization, I just want to point out that the two newly used methods are in the utils class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
namespace Delegates; public class ChangeUtils { public void SaveChangeToList() { Console.WriteLine("Change saved to list"); } public void PrioritizeChange() { Console.WriteLine("Change Prioritized"); } public void DoPaperWork(ChangeDetails changeDetails) { Console.WriteLine("Do paper work for {0}", changeDetails.Name); } public void CheckPaperWork(ChangeDetails changeDetails) { Console.WriteLine("Check paper work for {0}", changeDetails.Name); } public void CleanUpAfterPaperWork(ChangeDetails changeDetails) { Console.WriteLine("Clean up after paper work for {0}", changeDetails.Name); } public bool Process(ChangeDetails changeDetails) { return changeDetails.Valid; } }
If we run the code, we will see that the entire chain of callback methods was executed.
Lambda
Defining functions is cool and trendy, I know. But we don’t always have to do it so explicitly. C# has the option to create anonymous functions and lambda expressions. I’m not going to teach you about lambda expression itself because the topic is too vast for it. If you are not familiar with it, please visit a link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions. What I want to show you is that we can have the same functionality as before but with lambdas instead of defined functions.
The refactored code is going to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
var change = new ChangeDetails() { Id = 1, Name = "Name 1", Valid = true }; var changer = new MakeChanges(); var changeUtils = new ChangeUtils(); changer.OnChangeValidated += changeUtils.SaveChangeToList; changer.OnChangeValidated += changeUtils.PrioritizeChange; changer.OnProcessing += changeUtils.Process; Action<ChangeDetails> processCompletedChain = (changeDetails) => { Console.WriteLine("Lambda: Do paper work for {0}", changeDetails.Name); }; processCompletedChain += (changeDetails) => { Console.WriteLine("Lambda: Check paper work for {0}", changeDetails.Name); }; processCompletedChain += (changeDetails) => { Console.WriteLine("Lambda: Clean up after paper work for {0}", changeDetails.Name); }; changer.Process(change, processCompletedChain);
If we run the code, we will see that the chain call still works 100% correctly.
Local Cuisines
Using anonymous functions has one huge benefit over using defined once: anonymous functions are free to use local variables. Yes, yes, I know it may not be the best practice sometimes. But there are situations when we don’t have any other choice, or the choice is very costly. So, you should think about the local variables option like of card up in your sleeve.
The code for this little trick is going to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
var change = new ChangeDetails() { Id = 1, Name = "Name 1", Valid = true }; var changer = new MakeChanges(); var changeUtils = new ChangeUtils(); changer.OnChangeValidated += changeUtils.SaveChangeToList; changer.OnChangeValidated += changeUtils.PrioritizeChange; changer.OnProcessing += changeUtils.Process; var local = "LOCAL VAR "; Action<ChangeDetails> processCompletedChain = (changeDetails) => { Console.WriteLine("Lambda + Local: Do paper work for {0} _ {1}", changeDetails.Name,local); }; processCompletedChain += (changeDetails) => { Console.WriteLine("Lambda + Local: Check paper work for {0} _ {1}", changeDetails.Name,local); }; processCompletedChain += (changeDetails) => { Console.WriteLine("Lambda + Local: Clean up after paper work for {0} _ {1}", changeDetails.Name,local); }; changer.Process(change, processCompletedChain);
...and again, the chain defined this way is going to work just fine.
Summary
This was the second article in the Advanced C# series. We have refactored the previous code, so now it uses a set of built-in elements and is able to chain callback functions. Additionally, the option to use anonymous functions was presented. Now, the code is much clearer and flexible, and this should always be a good coder’s goal.
If you have any questions, please drop me a line at karol.rogowski@softwarehut.com.
Till next time. Keep coding.