Design Patterns - The Chain of Responsibility
What is a chain of responsibility and how do I use it?
In my last entry I stated that a strategy pattern described one solution to the problem of dealing with alternative implementations at run time. The Chain of Responsibility describes another pattern whose intent is to tackle the same basic problem.
How do I recognize where I need a chain of responsibility?
The formal definition of the chain of responsibility, as given by the “GoF” is:
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
In the previous example we showed how to use a strategy to cope with the problem of applying a location-specific rate of Sales Tax at run time. However, as we have seen in order to implement a strategy some object, somewhere, has to decide, at run time, which of the possible sub classes to implement. This may not always be either desirable, or even possible.
In a chain of responsibility each object knows how to evaluate a request for action and, if it cannot handle the request itself, knows only how to pass it on to another object, hence the ‘chain’. The consequence is that the client (which initiates the request for action) now only needs to know about the very first object in the chain. Moreover, each object in the chain only needs to know about one object too, the next one in the chain. The chain of responsibility can either be implemented using a predefined, or static, chain or the chain can be dynamic, built at run time by having each object add its own successor when necessary.
What are the components of a chain of responsibility?
A chain of responsibility can be implemented by creating an abstract ‘handler’ class (to specify the interface and generic functionality) and then by creating concrete sub classes to define the various possible implementations. However, there is no absolute requirement for all members of a chain of responsibility to be descended from the same class, providing that they all support the necessary interfaces to integrate with other members of the chain.
Client objects will need a reference to the specific sub class which is their individual entry point into the chain. However, there is no requirement that states that all clients must use the same entry point.
Yet again, we see the basic bridge pattern here. This is because each link in the chain is actually a bridge between an abstraction and an implementation. All that is different from the simple bridge is that a single object can play both roles depending on its situation.
Thus the first link has the client as the abstraction, and the first handler object as the implementation. However, the second link now has the first handler playing the role of the abstraction, with itself as the implementation. In turn, the second link becomes the abstraction for the third link’s implementation. This pattern can, in theory, at least, be repeated ad infinitum.
In order to implement a chain of responsibility pattern we first need to define an abstract handler class and as many different specific sub classes as needed. The key difference between Chain of Responsibility and the strategy pattern is that the abstract class defines the mechanism by which an object can determine whether it should handle the request. Additionally it requires a property to hold a reference to the next object in the chain. As stated earlier, there is no absolute requirement for all handlers to inherit from the same abstract handler, providing that they adhere to the minimum defined interface.
How do I implement a chain of responsibility?
The question that I posed in the context of the Strategy pattern was ‘How to address the issue of calculating sales tax when the calculation depends on the location of the sale’. You will recall that the solution was to define specific sub-classes for handling each discrete tax rate, and then to determine which sub-class was required based upon a table that linked location to applicable rate.
To handle the same problem with a chain of responsibility we can use the original tax calculation class that we defined for the Strategy and add the following new properties :
- cCanHandle: Defines the context handled by this sub class
- cNextObj: Name of the class to instantiate as next object in the chain
- cNextObjLib: Class Library for next object in the chain
- oNext: Object Reference to the Next Object in the chain
The ‘cCanHandle’ property defines the “context” that the specific instance will accept, while the ‘next’ properties are used to define which object should follow this one. These could be either pre-populated to create a pre-defined chain, or we could even determine the relevant values at run time to create an infinitely extensible chain that changes according to need.
For example, suppose the first object’s context were defined as a tax rate of 7.00% then it could determine that if passed a context that is greater than its own, that there is no point in instantiating an object with a context value less than its own. How do we implement that? One way would be to have a table that lists for each context the relevant class (and library). Then any object in the chain could simply look up the information as needed.
In addition to the properties described above, we need at least two methods:
- ProcessRequest: Exposed method that is used call the object and that determines whether a specific request is processed by this object
- CalcTax: The actual method that performs the calculation and returns the result
The client object must either create, or obtain, a reference to the first object in the chain. It will then call that object’s ProcessRequest() method and pass the context and the sale price for which the tax is required. This code assumes that the Chain of Responsibility will return either a valid Tax Amount, or a NULL:
WITH ThisForm
*** Check that we have the first object in the chain available
IF VARTYPE( This.oCalc ) # "O"
*** We don't so create it
.oCalc = NEWOBJECT( 'TaxChain01', 'chor.vcx' )
ENDIF
*** Now just call ProcessRequest() method and pass both context and price
lnTax = .oCalc.ProcessRequest( cContext, nSalePrice )
IF ISNULL( lnTax )
*** Couldn't process the Request at all
MESSAGEBOX( 'Unable to Process this Location', 16, 'Failed' )
lnTax = 0
ENDIF
RETURN lnTax
ENDWITH
*** Check that we have the first object in the chain available
IF VARTYPE( This.oCalc ) # "O"
*** We don't so create it
.oCalc = NEWOBJECT( 'TaxChain01', 'chor.vcx' )
ENDIF
*** Now just call ProcessRequest() method and pass both context and price
lnTax = .oCalc.ProcessRequest( cContext, nSalePrice )
IF ISNULL( lnTax )
*** Couldn't process the Request at all
MESSAGEBOX( 'Unable to Process this Location', 16, 'Failed' )
lnTax = 0
ENDIF
RETURN lnTax
ENDWITH
The code in the ProcessRequest() method is defined at the abstract class level and is completely generic. It is responsible for determining whether the incoming request is to be handled locally. If so, it simply calls the default CalcTax() method (also defined in the abstract class which uses the passed in price and the embedded tax rate). If not, the action taken depends upon whether another object is defined, and available, to handle the request, as follows:
LPARAMETERS tcContext, tnPrice
LOCAL lnTax
WITH This
*** Can we deal with the request here?
lcCanHandle = CHRTRAN( .cCanHandle, "'", "" )
IF tcContext == lcCanHandle
*** Yes, so call standard CalcTax() method, passing Price
lnTax = .CalcTax( tnPrice )
ELSE
*** We cannot deal with it, Do we have an object defined to pass it on to?
IF NOT EMPTY( .cNextObj ) AND NOT EMPTY( .cNextObjLib )
*** Yes we do. but does it already exist
IF VARTYPE( This.oNext ) # "O"
*** Create the object and call it
.oNext = NEWOBJECT( .cNextObj, .cNextObjLib )
ENDIF
*** And just call on the specified object
lnTax = This.oNext.ProcessRequest( tcContext, tnPrice )
ELSE
*** Nowhere else to go, just Return NULL
lnTax = NULL
ENDIF
ENDIF
RETURN lnTax
ENDWITH
LOCAL lnTax
WITH This
*** Can we deal with the request here?
lcCanHandle = CHRTRAN( .cCanHandle, "'", "" )
IF tcContext == lcCanHandle
*** Yes, so call standard CalcTax() method, passing Price
lnTax = .CalcTax( tnPrice )
ELSE
*** We cannot deal with it, Do we have an object defined to pass it on to?
IF NOT EMPTY( .cNextObj ) AND NOT EMPTY( .cNextObjLib )
*** Yes we do. but does it already exist
IF VARTYPE( This.oNext ) # "O"
*** Create the object and call it
.oNext = NEWOBJECT( .cNextObj, .cNextObjLib )
ENDIF
*** And just call on the specified object
lnTax = This.oNext.ProcessRequest( tcContext, tnPrice )
ELSE
*** Nowhere else to go, just Return NULL
lnTax = NULL
ENDIF
ENDIF
RETURN lnTax
ENDWITH
This is all the code that is needed and the individual sub classes for this very simple example need no custom code at all, everything is handled by setting properties (including the nTaxRate property defined in the original root class).
When should I use a Chain instead of a Strategy?
You may be wondering, at this point, ‘why bother?’ – especially since we have already seen a perfectly good and easily data-driven solution to the issue in the Strategy Pattern. However, the limitation of the strategy pattern is that only ONE sub class can exist, and therefore it is not appropriate when we need multiple operations. The example used here is for a one-shot chain! As soon as an object handles the task the result is returned and no other object is needed.
However, the Chain of Responsibility really comes into its own when multiple operations may be required. An extension of our simple Tax problem would be to handle issues like “Shipping & Handling Charges”, “Discount” or even multiple taxes (e.g. local surcharges).
In this situation a Strategy is not enough, but the Chain of Responsibility handles it easily. All that is needed is, instead of stopping at the first object that can handle the task, we pass the request on to every object in the chain unequivocally, and allow each the opportunity to contribute to the final solution. Of course, we may need more than a single return value in such a scenario, but parameter objects provide a simple and convenient way of handling that too.
So now we would have objects in our chain based on the “Sales Tax Calculator”, the “Shipping Calculator” and “Discount Calculator” classes. All they need is the relevant PEMs to participate in the chain.
Chain of responsibility pattern summary
The chain of responsibility provides us with another way of resolving the problem of providing functionality without the necessity to code it explicitly. Perhaps the biggest benefit of the chain of responsibility is the ease with which it can be extended, while its main drawback is that it can dramatically increase the number of objects active in the system. As always, the final word is to remind you that even though the actual implementation details may vary, the pattern itself does not change.
Published Sunday, December 24, 2006 3:23 PM by andykr
No comments:
Post a Comment