FAQ Articles Links Personal

Chain of Responsibility

 

Scope

Implementing a generic way to use the Chain of Responsibility design pattern.

The Chain of Responsibility (CoR) allows decoupling data from algorithms that process it. Consider there are more algorithms that could process the data, but only a few of them (often one) can process it correctly. However, having multiple algorithms in the same class as the data is not always a good idea. Also, it makes it hard to add certain algorithms.

This article will present the CoR pattern and elaborate a general way of using it discussing different approaches.

Introduction

A generic implementation of the CoR pattern should pose as little restriction to the data and algorithms as possible, while remaining safe to use. A good way of representing algorithms would be using a wrapper, such as a function object.

Using function objects (also known as functors) is a great way of generalizing algorithms. A function object can store non-member functions as well as member functions. With a good implementation, and the use of binders, it should be simple to compose functions or bind the object to the member function to be called.

The data is harder to abstract. The problem is that an algorithm might require any number of arguments. These arguments might have any type: they might have the same type or they might even require a single argument. However, such a restriction is not desirable. Holding any number of arguments of unknown types is a common use of tuples. The tuple data structure solves all our problems: it can hold any number of arguments, each of a different type.

I shall avoid reinventing the wheel of function objects and tuples by using the boost library, specifically the function, bind and tuple parts of it.

Designing a generic Chain of Responsibility implementation

Some design decisions were already presented in the introduction. I shall use boost::function to store algorithms and boost::tuple to store the data.

The remaining design decisions are:

Trying the algorithms in some predefined order, such as the order they were added, or the reverse order of that is too rigid. If a rule of the system is that only one algorithm can process a certain set of data, this would be fine. However, if this is not a case, sometimes we would like to have some algorithms be tried before others. For this reason, adding an algorithm would also require providing an ID which will be used to decide the order in which the algorithms are tried.

Restricting the way algorithms signal failure of data processing is not desirable. Some might want to throw an exceptions while others would rather return an error code. One way to avoid this problem is provide more Process() functions in our CoR implementation, such as ProcessWithExceptions() or ProcessWithErrorCode(). However, there are other ways to signal an error (for instance, using boost::optional). Also, the interface of the CoR implementation should be minimal. This is where policy-based design can really help. A policy can take care of the way error handling is done. Some useful ones can be provided by the implementation while giving users the opportunity to write their own.

What happens if no algorithm can process the data ? Do we throw an exception ? Return an error code ? This discussion seems pretty similar to the above "how does an algorithm signal it can't process the data", and as such, this will be also implemented using a policy.

Implementing the CoR, part one

Obviously, the CoR class should be able to take any type for the return type of the algorithm and any argument tuple. Also, the algorithm ID should be any comparable type. It also has two policies: ErrorPolicy and ProcessPolicy:

template <

typename Return,

typename ArgumentTuple,

typename Key = int,

template <class,class> class ErrorPolicy,

template <class,class,class,class>class ProcessPolicy >

class ChainOfResponsibility : public ProcessPolicy <

Return, ArgumentTuple,

std::map< Key, boost::function<Return (ArgumentTuple)> >,

ErrorPolicy< Return, ArgumentTuple > > {

public:

typedef Return ReturnType;

typedef ArgumentTuple ArgumentTupleType;

typedef Key KeyType;

typedef boost::function< ReturnType (ArgumentTupleType) > Functor;

 

bool RegisterFunction( KeyType key, Functor f );

bool UnregisterFunction( KeyType key )

ReturnType Process( ArgumentTupleType arg )

private:

typedef std::map< KeyType, Functor > MapType;

MapType chain_;

};

The RegisterFunction and UnregisterFunction members add or remove functors and the Process member uses the ProcessPolicy's DoProcess() member to do the actual work, which, in turn, uses the ErrorPolicy.

Implementing the CoR, part two

The Error Policy class provides a single member function that is called whenever no algorithm could process the data. It has two template types, ReturnType and ArgumentTupleType because someone might decide to log the data or return some error code. Of course, the ErrorPolicy can throw an exception. As the implementation is trivial, I shall skip this part.

The ProcessPolicy classes can be implemented using various techniques such as using exceptions, error return codes or boost::optional. Such a policy would generally look like this:

template <

typename Return, typename ArgumentTuple,

typename Map, typename ErrorPolicy >

struct ExceptionProcessPolicy : public ErrorPolicy {

public:

typedef Return ReturnType;

typedef ArgumentTuple ArgumentTupleType;

ReturnType DoProcess( const Map& chain_, ArgumentTupleType arg );

};

Implementing the CoR: the Exception Process Policy

The Exception Process Policy assumes each algorithm throws an exception if it can't handle the data. With this in mind, here's how the policy is implemented:

ReturnType DoProcess( const Map& chain_, ArgumentTupleType arg ) {

typename Map::const_iterator iter( chain_.begin() );

typename Map::const_iterator end( chain_.end() );

while( iter != end ) {

try {

ReturnType ret( (iter->second)(arg) );

return ret;

}

catch(...) { ++iter; }

}

return OnNoApropiateFunction( arg ); // from ErrorPolicy

}

The advantages of such a policy are:

The disadvantages are:

Implementing the CoR: the boost::optional Process Policy

Using boost::optional as the algorithm's return type poses a restriction on the algorithms, but is nonetheless a good way to signal whether the data was processed.

ReturnType DoProcess( const Map& chain_, ArgumentTupleType arg ) {

typename Map::const_iterator iter( chain_.begin() );

typename Map::const_iterator end( chain_.end() );

while( iter != end ) {

ReturnType ret = (iter->second)(arg);

if( ret ) // true if the object in boost::optional has been initialized

return *ret;

++iter;

}

return OnNoApropiateFunction( arg );

}

The advantages of such a policy are:

The disadvantages are:

Implementing the CoR: the Error Code Process Policy

Some algorithms would rather return a certain error code which usually is a value the processed data can never be equal to. Sometimes this is a good idea, especially if the processed data is stored in a user-defined type which has some way of testing whether the data is valid or not (somewhat similar to what boost::optional does).

ReturnType DoProcess( const Map& chain_, ArgumentTupleType arg ) {

typename Map::const_iterator iter( chain_.begin() );

typename Map::const_iterator end( chain_.end() );

while( iter != end ) {

ReturnType ret( (iter->second)(arg) );

if( ret != errorCode_ ) // errorCode_ is a member of the policy, set by a helper member function

return ret;

++iter;

}

return OnNoApropiateFunction( arg );

}

The advantages of such a policy are:

The disadvantages are:

Using the CoR implementation

This example uses classes Test1 and Test2 which have an integer return type and take a single argument, a std::string. The Process() function is called using a string because the tuple can be constructed if the number of members it holds is one. The default policies are used.

ChainOfResponsability< int, boost::tuples::tuple< const std::string& > > cor;

Test1 t1; Test2 t2; // classes that define Help member functions.

cor.RegisterFunction( 0, boost::bind( &Test1::Help, &t1, _1 ) );

cor.RegisterFunction( 1, boost::bind( &Test2::Help, &t2, _1 ) );

std::string str( "test" );

int ret = cor.Process( str );

Conclusion

Even though the Chain of Responsibilities design pattern is not very different from the Object Factory pattern, it poses a few interesting design decisions, especially regarding the process policies. Which one should be used doesn't have a general answer, which is why policies were used in the first place.

 

You can download the code here.

 

References:


© Ciobanu Vladimir, 2004