The idea behind Chain of Responsibility pattern is quite simple and powerful:
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.
You can find lots of object oriented implementations out there so as an exercise we will rather try to do it in a more functional way. For simplicity Func<T, R> will be considered as handler contract. The basic idea looks like this:
Func<T, R> h = t => { // Decide whether you can handle request bool canHandle = ...; // Get successor from somewhere Func<T, R> successor = ...; if (canHandle) // Handle request represented by t else // Delegate request to successor return successor(t); };
The first thing to solve is how to get successor. As handler must support composition it cannot simply create closure over successor. On the other hand it can be represented as function that returns actual handler closed over its successor:
Func<Func<T, R>, Func<T, R>> h = successor => t => { bool canHandle = ...; if (canHandle) // Handle request represented by t else // Delegate request to closed over successor return successor(t); };
Now we need to compose handlers into a chain.
// Creates chain of responsibility out of handlers static Func<T, R> Chain<T, R>(IEnumerable<Func<Func<T, R>, Func<T, R>>> handlers) { // By default if none of handlers can handle incoming request an exception is thrown Func<T, R> notSupported = t => { throw new NotSupportedException(); }; return Chain(handlers, notSupported); } // Creates chain of responsibility out of regular and default handlers static Func<T, R> Chain<T, R>(IEnumerable<Func<Func<T, R>, Func<T, R>>> handlers, Func<T, R> def) { // Assuming that order of handlers within the chains must be the same as in handlers sequence return handlers // Handlers needs to be reversed first or otherwise they will appear in the opposite order .Reverse() // Iteratively close each handler over its successor .Aggregate(def, (a, f) => f(a)); }
To make it more clear lets expand chaining of simple two handlers case:
// default handler Func<int, int> d = x => x; // two handlers appear in sequence in order of declaration Func<Func<int, int>, Func<int, int>> h1 = s => t => t < 10 ? t*2 : s(t); Func<Func<int, int>, Func<int, int>> h2 = s => t => t < 5 ? t + 3 : s(t); // 1. Reverse handlers // h2 // h1 // 2. Close h2 over d // tmp1 = t => t < 10 ? t * 2 : d(t); Func<int, int> tmp1 = h2(d); // 3. Close h1 over tmp1 // tmp2 = t => t < 5 ? t + 3 : tmp1(t); Func<int, int> tmp2 = h1(tmp1); // 4. tmp2 is the chain
Now handlers are dynamically composed into chains to address particular scenario.
As a chaining exercise let’s create the following application (a team of developers tries to handle a project):
- Project is divided into a number of task of complexity that doesn’t exceed particular threshold.
- In order to handle the project development team is staffed. A developer with skill X can handle task of complexity C when C <= X otherwise he contributes to task completion making task’s complexity smaller by X and delegates the rest of the task to next developer. Completed task is linked to developer who completed it.
- If the team cannot handle particular task they ask for help for an external expert.
Prepare
// Staff development team that will do the project static IEnumerable<Func<Func<int, int>, Func<int, int>>> Staff(int teamSize, int maxSkill) { var rnd = new Random(); for (int i = 0; i < teamSize; i++) { int dev = i; // Developers may differ in their skills int skill = rnd.Next(maxSkill); // If developer can handle incoming task he reports by returning his id that he completed the task // If not (not enough skills) he contributes to task and delegates to next developer smaller task yield return c => t => t <= skill ? dev : c(t - skill); } } // Create work break down structure for the project static IEnumerable<int> Work(int projectSize, int maxTaskComplexity) { var rnd = new Random(); for (int i = 0; i < projectSize; i++) { yield return rnd.Next(maxTaskComplexity) + 1; } }
and march to the end.
// Create tasks var work = Work(projectSize, maxTaskComplexity).ToArray(); // If the team cannot handle particular task they ask for help unknown guru Func<int, int> askForHelp = t => -1; // Create chain out of developers to form a team with a backup var team = Chain(Staff(teamSize, maxTaskComplexity), askForHelp); // Hand out each task to the team var project = from w in work select new {Task = w, CompletedBy = team(w)}; foreach(var status in project) { Console.WriteLine("Task {0} completed by {1}", status.Task, status.CompletedBy); }
Have chaining fun!
No comments:
Post a Comment