- Template Method Pattern Revisited
- Passing the Buck: Chain of Responsibility
- Conclusion
- Notes and References
Passing the Buck: Chain of Responsibility
Chain of Responsibility (CoR) passes a sender request along a chain of potential receivers, such as decoupling the sender from the receiver by giving more than one object a chance to handle the request. See reference [6] for a description of how CoR might help network event management. The implementation is straightforward, closely following [3]. There is an EventHandler interface implemented by three different classes in a hierarchy (Connection, Lsp, Path) with each class representing a different abstraction stage of a connection:
class EventHandler { public: explicit EventHandler(EventHandler* = 0, Event = No_Event_Support); virtual const bool HasEventSupport(); virtual void SetHandler(EventHandler*, Event); virtual void HandleEvent(); virtual ~EventHandler() { }; private: EventHandler* _successor; Event _event; };
CoR links these classes so that if the main connection fails the next one in the chain takes over the load a.s.o.
There is a certain critique related to the "classic" CoR—mainly related to the fact that the mechanism of "passing the buck" is not encapsulated but left to be (re)implemented every time by the user in every ConcreteHandler [3]. This is error-prone (see [7] for details). There is also the virtual mechanism that might incur penalties in certain conditions, as discussed in Part 1 of this series.
How can CoR be implemented using signature-based polymorphism? After going through strategy and template methods SBP implementations are not hard with the following code (see handler_chain.h for all the pieces):
struct Connection { template <class V> bool handleEvent(V* v ) const { std::cout << "Connection\n" ; return *v < 1 ? true : false; } }; struct Lsp { template <class V> bool handleEvent(V* v ) const { std::cout << "Lsp\n" ; return *v < 3 ? true : false; } }; struct Path { template <class V> bool handleEvent( V* v ) const { std::cout << "Path\n" ; return *v < 5 ? true : false; } };
These are the three stages abstracted to a common implicit interface and seen as nodes in a CoR. A NodeBase<NODE, ADAPTER> further abstracts the concept of a node in a CoR by doing the following:
- Keeping all the CoR functionality in one place and avoiding the need to spread details of the advancing mechanism in nodes
- Passing each node’s functionality through an adapter as an extra level of indirection
- Keeping track of each node’s relationship inside the chain, which is the service that uses SBP to hide the type of the link
template <class NODE, class ADAPTER> struct NodeBase { private: typedef void (*FP1)(void* , void*); public: typedef typename ADAPTER::RET_TYPE RET_TYPE; typedef typename ADAPTER::VALUE_TYPE VALUE_TYPE; NodeBase() : p_(0), fn_(0) {} template <class T> void setNext(T& x) { p_ = &x; fn_ = &functions<T>::processImpl; } void operator()(VALUE_TYPE* v) { if( ADAPTER()(NODE(), v ) ) return; if (p_==0 || fn_== 0) { std::cout << "No Result Found\n"; return; } fn_(p_, v); } private: template <class TT> struct functions { static void processImpl( void* a , void* v) { (*static_cast<TT*>(a))(static_cast<VALUE_TYPE*>(v)); } }; void* p_; FP1 fn_; };
setNext() is the call that establishes another node, of a different type, as the successor of the current one. And speaking of the current node: for simplicity, NODE was considered stateless, but more sophisticated models are possible.
From the NodeBase definition, we can easily deduce that the adapter has to provide additional type information with the same functionality as the original code:
template <typename V, typename R> struct Adapter { typedef R RET_TYPE; typedef V VALUE_TYPE; template <class H> RET_TYPE operator()( const H& h, VALUE_TYPE* v) const { return h.handleEvent(v); } }; And now we can write code like: typedef Adapter<int, bool> ADAPTER; NodeBase<Connection, ADAPTER> connection; NodeBase<Lsp, ADAPTER> lsp; NodeBase<Path, ADAPTER> path; connection.setNext(lsp); lsp.setNext(path); int k = 1; connection(&k);
Of course, a class similar to ITemplate (called INode in our example) can be added for additional functionality, based on a unique interface:
template <class Head> INode* createChain( Head& head) { return new INode(head); }
Now it is possible to manipulate the whole CoR using INode only:
std::auto_ptr<INode> i(createChain(connection)); k = 10; (*i)(&k);