Udostępnij za pośrednictwem


Chaining responsibilities

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!