The decorator describes a solution to the problem of adding functionality to an object without actually changing any of the code in that object.
How do I recognize where I need a decorator?
The formal definition of the strategy, as given by “Design Patterns, Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides is:
Attach additional responsibilities to an object dynamically
The need to dynamically alter the functionality (or behavior) of an object arises in either of two situations. First, where the source code for the object is simply not available; perhaps the object is an ActiveX control or a third party class library is being used. Second, where the class is widely used but the specific responsibility is only needed in one or two particular situations and to simply add the code to the class would not be appropriate.
In the preceding articles we have looked at various ways of determining the amount of tax to apply to a ‘price’ depending on the ‘location’. The basic tax calculation object in every case works by exposing a method named CalcTax() which takes two parameters, a value and a rate, and returns the tax due. We tackled the basic problem of dealing with tax in a form by using the bridge pattern to separate the implementation from the interface. However, as we quickly saw, although more flexible than simply coding it directly, it was not flexible enough to cope with different tax rates in different locations. Both Strategy and Chain of Responsibility can handle the issue, however, both solutions involve sub classing.
The decorator pattern allows us to solve the same problem without the need to sub class the original. Instead we define a new object which has exactly the same interface as the tax calculator, but which includes the necessary code to determine the appropriate tax rate for a given location. This object ‘looks’ just like the tax calculator to its client, but, because it also holds a reference to the real tax calculator, it can “pre-process” any request for a tax rate, and then simply call the real implementation when ready.
What are the components of a decorator?
A decorator has two essential requirements. First we need the implementation class that defines the interface, and the core functionality. Second, we need a decorator that reproduces the interface of the implementation, and holds a reference to it. The client object now directs calls that would have gone directly to the implementation to the decorator object instead. The basic structure for the decorator pattern is:
This pattern is actually an extended bridge. As far as the client is concerned it can address the decorator object as if it really were the implementation at the end of a standard bridge because the interface of the two is the same. As far as the implementation is concerned, the request looks exactly the same as if it had come from directly from the client. In other words, it does not ever need to know that the decorator even exists.
How do I implement a decorator?
A new class, named “cntDecorator”, has been defined to implement the decorator example. It has three custom properties and one custom method as shown:
Name Description
cImpclass Name of the class to instantiate for this decorator
cImplib Class Library for the implementation class to instantiate
oCalculator Object reference to an instance of the class which is the real
implementer of the CalcTax() method.
This object is instantiated in the decorator’s Init()
CalcTax The decorator’s implementation for the equivalent method
on the ‘real’ implementer
The code in the decorator’s CalcTax() method is quite straightforward. It expects to receive two parameters, the location ID as a string, and the price for which the tax is required. Notice that these are not the same parameters that are required by the CalcTax() method in the real class (Over there we need to pass a price and a rate). The decorator’s CalcTax() method determines the appropriate rate based on the location ID and then calls its implementation object’s CalcTax() method passing the ‘real’ parameters. The return value is just passed back to the client without any further modification.
LPARAMETERS tcLocation, tnPrice
LOCAL lnRate, lnTax
STORE 0 TO lnRate, lnTax
*** Determine the correct rate
DO CASE
CASE tcLocation = '01'
lnRate = 5.75
CASE tcLocation = '02'
lnRate = 5.25
CASE tcLocation = '03'
lnRate = 0.00
OTHERWISE
lnRate = 0
LOCAL lnRate, lnTax
STORE 0 TO lnRate, lnTax
*** Determine the correct rate
DO CASE
CASE tcLocation = '01'
lnRate = 5.75
CASE tcLocation = '02'
lnRate = 5.25
CASE tcLocation = '03'
lnRate = 0.00
OTHERWISE
lnRate = 0
ENDCASE
*** Now pass on the call to the real object
lnTax = This.oCalculator.CalcTax( tnPrice, lnRate )
lnTax = This.oCalculator.CalcTax( tnPrice, lnRate )
*** And return the result
RETURN lnTax
RETURN lnTax
In order to use the decorator, all that is needed is a small change to the client. Instead of instantiating the real calculation object it must, instead, instantiate the decorator object. However, remember that the calling signature for the decorator’s method is identical to that for the ‘real’ method, so no other change is needed.
LPARAMETERS tcContext, tnPrice
LOCAL lnTax
WITH ThisForm
*** Check that we have the Decorator available
IF VARTYPE( This.oCalc ) # "O"
*** We don't so create it
.oCalc = NEWOBJECT( 'cntDecorator', 'calclass.vcx' )
ENDIF
*** Now just call it's CalcTax() method and pass both location and price
lnTax = .oCalc.CalcTax( tcContext, tnPrice )
IF ISNULL( lnTax )
*** Couldn't process the Request at all
MESSAGEBOX( 'Unable to Process this Location', 16, 'Failed' )
lnTax = 0
ENDIF
RETURN lnTax
ENDWITH
LOCAL lnTax
WITH ThisForm
*** Check that we have the Decorator available
IF VARTYPE( This.oCalc ) # "O"
*** We don't so create it
.oCalc = NEWOBJECT( 'cntDecorator', 'calclass.vcx' )
ENDIF
*** Now just call it's CalcTax() method and pass both location and price
lnTax = .oCalc.CalcTax( tcContext, tnPrice )
IF ISNULL( lnTax )
*** Couldn't process the Request at all
MESSAGEBOX( 'Unable to Process this Location', 16, 'Failed' )
lnTax = 0
ENDIF
RETURN lnTax
ENDWITH
Although this is a very simple example, you can see how easy it would be to extend the functionality of the decorator. One very common real life problem that this pattern can be used to address is how to provide implementation-specific validation to a generic save routine.
Decorator pattern summary
The decorator pattern is used when we wish to modify the basic behavior of a specific instance without the necessity of either creating a new sub class, or changing the code in the original. Essentially the decorator acts as a pre-processor for its implementation by interposing itself between its client and the implementation.
Published Sunday, December 31, 2006 12:25 PM by andykr
No comments:
Post a Comment