Decorating for the Busy Developer
This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
Decorating for the Busy Developer
Decorating or "wrapping" an object represents a flexible alternative to subclassing. In this article, Lauren Clarke explains what a decorator is, demonstrates a novel way to implement the pattern in Visual FoxPro, and provides some application ideas.
If you're like me, your eyes tend to glaze over a bit when object-oriented design pattern terms are thrown about. The word "decorator" is one of those terms, and due in part to the image of Martha Stewart it immediately conjures up, and the complete lack of tasteful decorations in most programmers' lairs, it leaves one wondering what decorations and programming have to do with each other. Decorator (a noun) is a design pattern that uses aggregation (another OOP term, which means one object holds a reference to another) to "wrap" an object and perhaps provide some additional features not present in the wrapped object. Simply put, a decorator is a class that provides a "skin" around another object to which one wishes to add new responsibilities at runtime. A simple example follows.
Consider the object that SCATTER NAME creates in VFP:
SCATTER NAME loData
This line of code creates an object with properties that correspond to the fields of the table in the current workarea, and property values corresponding to the values of those fields. This process represents a nice, lightweight alternative to assigning the properties of a custom data object from the fields of a table. However, wouldn't it be nice if loData could do something other than store field values? It'd be nice if it had methods you could use in your day-to-day life as a developer working with this object. For example, what if it could validate itself, or render itself as an XML string, or save itself? The trouble is that we don't have access to the internal Visual FoxPro class used to create loData. We can't create subclasses of it, and even if we could, we couldn't force SCATTER NAME to use our subclass instead of the base class. So we have an object that we'd like to endow... decorate... with new abilities, and we can't (or don't want to) create a subclass to deal with these new responsibilities. So, we're going to implement a method of extending the abilities of the SCATTER NAME object at runtime. Enter the decorator pattern.
DEFINE CLASS DecoCust1 AS Relation *-- data properties cFName = '' cLName='' cPhone= '' nCredLine = '' *--- other properties oData = NULL cFullName = '' FUNCTION cFName_Assign( tcValue) THIS.oData.cFName=tcValue FUNCTION cFName_Access() RETURN This.oData.cFName FUNCTION cPhone_Assign( tcValue) THIS.oData.cPhone=tcValue FUNCTION cPhone_Access() RETURN This.oData.cPhone FUNCTION cLName_Assign( tcValue) This.oData.cLName=tcValue FUNCTION clName_Access() RETURN This.oData.cLName FUNCTION nCredLine_Assign( tcValue) This.oData.nCredLine=tcValue FUNCTION nCredLine_Access() RETURN This.oData.nCredLine FUNCTION cFullName_Assign( tcValue) LOCAL lnPos lnPos = AT( " ",tcValue) This.oData.cFname = LEFT(tcValue,lnPos) This.oData.cLname = Substr(This.oData.cFname,lnpos,50) FUNCTION cFullName_Access() RETURN This.oData.cFname+" "+This.odata.cLname FUNCTION INIT( toData) This.oData = toData FUNCTION isValid() *-- a validation code goes here *-- see downloads file for code FUNCTION toXML() *-- a xml string building code goes here *-- see downloads file for code FUNCTION Save() GATHER This.oData FUNCTION DESTROY() This.oData = NULL ENDDEFINE
Well, that was a bunch of work (but don't worry, I'm going to show you a way to avoid most of it). Let's go over the highlights of this class. First, note that we have a property for each field in our customer table. Also, we have a property oData that will hold a reference to our SCATTER NAME generated data object. Finally, we have a property cFullName that will provide the customer's full name based on the cFname and cLname fields. Then, we have a bevy of access and assign methods that serve to link the oData properties to the decorator's properties. With this construct in place, the following expressions will both return the same value:
The access and assign approach ties us to version 6.0 or later of VFP. The alternative is to use GetProp() and SetProp() methods to provide access to the wrapped object. For example, we could have a GetFname() function that would return oData.cFname, but this really makes the wrapper less than transparent, which isn't desirable. Finally, note that the INIT() method takes as a parameter a reference to the data object to be wrapped by the class. Okay, let's put our decorator to work.
We'll dissect the following code snippet line by line:
SCATTER NAME loData BLANK loData.cFName = "Mellow" loData.cLname = "Yellow" loData = CREATE("DecoCust1",loData) If loData.IsValid() APPEND BLANK loData.Save() ELSE …
First, we create a standard SCATTER NAME object. The BLANK keyword indicates the object will be created with empty properties.
SCATTER NAME loData BLANK
Next, we assign our new object a first and last name.
loData.cFName = "Mellow" loData.cLname = "Yellow"
Nothing spectacular so far, but this next line is the point at which the rabbit goes into the hat.
loData = CREATE("DecoCust1",loData)
Here, we instantiate a class DecoCust1, the init function of this class takes an object as a parameter, and we send our SCATTER NAME generated loData object for this parameter. DecoCust1 wraps itself around loData and augments its abilities. Note that we reuse the loData variable name to store a pointer to our new instance of DecoCust1. This isn't necessary, but since DecoCust1 will seamlessly wrap our data object, presenting all of its original properties as if it were the old loData, it's natural to do so. In our case, DecoCust1 has an IsValid() method, and we can use this to validate Mr. Yellow.
If loData.IsValid() …
If we pass this test, a record is added to the current table and we fire the Save() method, which will save the record to the table.
APPEND BLANK loData.Save() ELSE …
Now that the object has been created and is decorated with additional properties and methods, we can check our record with IsValid(), save it with Save(), render it to XML with toXML(), and all the while we can still reference the objects' original properties:
? loData.cFName && prints "Mellow" ? loData.cLName && prints "Yellow"
And we also have an additional property calculated from underlying fields.
? loData.cFullName && prints "Mellow Yellow"
To summarize, we have a class DecoCust1, which adds a cFullName property, IsValid(), Save(), and toXML() methods (and potentially many other useful methods) to our SCATTER NAME generated data object. From the developer's standpoint, there's very little practical difference between the wrapper and a bona fide subclass of the Visual FoxPro data object class. The key point, of course, is that you can't subclass Visual FoxPro's data object class. Also note that these abilities were added at runtime, meaning we have the option of adding these abilities if and when they're needed without instantiating a large feature-ridden class each time a record object is needed.
Another real-world sample of a decorator, csZIP/csUnZip, is available in the accompanying Download file. It decorates the popular DynaZip utility to provide some simplifications and extensions that are helpful when using the utility from within VFP.
Subclasses vs. decorators
Decorators offer an alternative to subclassing, but they must be used judiciously. The advantages come from the fact that you can gain subclass-like behavior for classes that you can't actually subclass. Also, you're given more flexibility at runtime to endow your objects with abilities only if they need it to fulfill the current task. In our example, there might be many places in an application where the base FoxPro data object will suffice. This being the case, it would be a shame to have to instantiate and use a complicated and expensive class where the lightweight class would do. Decorators offer "pay as you go" options where one can add functionality as needed. It's possible to nest decorators inside decorators. If we decided that mixing the validation code and the XML rendering code in one class made it too large and inflexible, we could create a separate decorator for each task. For example, we could start with the basic data object:
SCATTER NAME loData BLANK
Then, if necessary, endow it with the ability to validate its properties:
loData = CREATE("ValidateDataDeco",loData) loData.ValidateProperty("cFname")
Then, if needed, we could decorate again and gain some rendering functionality:
loData = CREATE("RenderDataDeco",loData)
Which would allow us to do things like this:
And, unless you need to validate and render your data object every time you use it, you can save a lot of overhead by avoiding the giant do-everything data class.
The following lists summarize the pros and cons.
Advantages of decorators:
- They allow extension of classes that we can't directly subclass.
- They allow us to avoid deep class hierarchies.
- They provide a pay-as-you-go option, which avoids instantiating large feature-ridden classes when few of the features are needed.
Disadvantages of decorators:
- Pass-through code must be maintained.
- Passing requests through to the decorated component requires a performance hit.
- They can complicate debugging.
From the developer's perspective, the single most important issue here is the maintenance of pass-through code. The access and assign code must be maintained in concert with the wrapped object. In our example, this means that each time the structure of the table changes, we've got some work to do in our decorator class definition. This issue is exacerbated when we're wrapping classes that have methods as well as properties, as we have to write pass-through code for each method. In short, if the interface of the wrapped object changes, so must the wrapper. Until recently, this fact was enough to really cool one's feet to the idea of using the decorator in anything but the most dire situations. However, version 6.0 of Visual FoxPro gives us an opportunity to generalize decorator classes and completely eliminate this fragile use-case specific pass-through code. Our rescue comes in the form of the THIS_ACCESS method.
THIS_ACCESS is a method that can be added to any subclass in VFP. This method will fire every time the class is accessed. This means that every time a property is set or accessed or a method is called, the THIS_ACCESS method will fire prior to that action taking place. THIS_ACCESS takes as a parameter the name of the member being accessed. (Side note: It's too bad that THIS_ACCESS only takes the called member as a parameter; if one could also access the value being sent [in the case of a property assignment] or the parameters being sent [in the case of a method call] inside the THIS_ACCESS method, it would open a world of possibilities, but that's off topic for this article.) THIS_ACCESS must also return a reference to the object being accessed. It's this last requirement that we leverage to implement an almost codeless delegation scheme for a generic decorator class. Let's redo our prior example using this new approach.
Redecorating with THIS_ACCESS
Here's what the class definition for DecoCust might look like when we utilize THIS_ACCESS:
DEFINE CLASS DecoCust2 as RELATION oData = NULL FUNCTION INIT( toData) *-- add parameter checking here THIS.oData = toData ENDFUNC FUNCTION THIS_ACCESS( tcMember) IF PEMSTATUS(THIS,UPPER(tcMember),5) RETURN THIS ELSE RETURN THIS.oData ENDIF ENDFUNC FUNCTION DESTROY() THIS.oData = NULL ENDDEFINE FUNCTION isValid() *-- validation code goes here FUNCTION toXML() *-- xml string building code goes here ENDDEFINE
That's it. Notice the substantial reduction in lines of code from our previous DecoCust1 example. The "big idea" here is the THIS_ACCESS method that first checks to see whether the requested member belongs to the decorator class, and, if not, a reference to the wrapped data object is returned. This way, the decorator can decorate by adding functionality like this IsValid() while forwarding requests for the oData properties directly to the oData object.
Also, notice that the DecoCust2 class is very generic. The IsValid() and toXML() methods could be removed and we'd have a nice BaseDeco class to wrap any component that we could subclass to add things like IsValid() for specific implementations.
Paying the piper
Wrapping a class has some costs in terms of both development and runtime. The development costs come from the need to keep the interface of the wrapper synchronized with the interface of the wrapped component. If you choose to manually maintain the interface, this can be a costly proposition—especially if the wrapped class is changing often. Using the aforementioned THIS_ACCESS trick can vastly reduce your development load, as the interface will be updated automatically. However, since THIS_ACCESS fires each time the object is used, there's a runtime cost to be paid for this approach. Table 1 will give you an idea of the runtime costs for these different approaches.
Table 1. The different approaches and their associated runtime costs.
|Access decorator property||1||0.95||9.90|
|Assign decorator property||1||2.20||11.34|
|Call decorator method||1||0.96||4.48|
|Access decorated property||1||0.96||6.81|
|Assign decorated property||1||1.14||5.37|
|Call decorated method||1||2.15||4.88|
This table has been normalized to be machine-independent and more readable. For each task, the "Subclass" option has been given a weight of 1 and the others scaled accordingly. So, for example, Deco2 takes 9.90 times longer to access a property than a traditional subclass. To get actual time values for your system, just run the perfcheck.prg provided in the Download file.
Some of these factors look pretty alarming, but keep in mind the times we're talking about here. My system, a PIII that's limping along at 500 Mhz, takes 0.000006 seconds to access a property from Deco1, and a staggering 0.00003 to access the same property through a decorator using THIS_ACCESS (Deco2). In a real use-case, say a middle-tier data object, an application might access a data object 100 times to serve a user's request. In this situation, the THIS_ACCESS method represents a cost of no more than 0.003 seconds in our benchmark classes. Considering the THIS_ACCESS method might eliminate hundreds of lines of high-maintenance pass-through code, this might represent a good tradeoff. However, these results do make one pause to consider carefully where to implement these techniques.
WITH/ENDWITH and GATHER gotchas
If you plan on using a THIS_ACCESS decorated class in a WITH/ENDWITH loop, you'll be in for a surprise. VFP exhibits some peculiar behavior in this area. In the April 2001 issue of FoxTalk, Randy Pearson wrote an article on the advantages of the WITH/ENDWITH command. It's likely you'll want to use this construct with a decorated class at some point. The trouble is that VFP won't let you. The following code won't fire the THIS_ACCESS method of loData and will result in an error:
SCATTER NAME loData BLANK loData = CREATE("DecoCust1",loData) WITH loData .cFname = "Billy" .cLName="Bob" .cPhone="123.456.7890" ENDWITH
A workaround is to reference the decorated component directly in the WITH construct:
WITH loData.oData .cFname = "Billy" .cLName="Bob" .cPhone="123.456.7890" ENDWITH
There are some differences in the behavior here between VFP 6 and VFP 7 Beta 1. Both are odd and not really consistent with the documentation on access and assign methods. There's a program in the Download file you can use to explore the differences.
In addition to this, while the GATHER NAME command works fine with the property-level access methods, it seems to ignore the THIS_ACCESS method at this time.
The decorator pattern offers a nice alternative to subclassing. The THIS_ACCESS method of building decorators allows us to avoid writing reams of pass-through code when building decorators in VFP. This convenience comes with a performance price, but in many situations I think the price is more than justified. I'll leave you with one possibly interesting diversion. Look up "multiple inheritance" in a good general OOP reference. Then, take a look at our DecoCust2 class, and consider the possibility of aggregating more than one object at a time and replacing the IF/THEN in the INIT() clause with a CASE statement. Bon voyage!
(Lauren thanks the http://fox.wikis.com community for their help in refining and testing some of the ideas presented in this article.)
- Design Patterns, Elements of Object Oriented Software, by E. Gamma, R. Helm, R. Johnson, and J. Vlissides (Addison Wesley, 1994, ISBN 0201633612).
- "Simulating Multiple Inheritance," by Michael Malak, in the April 2001 issue of Journal of Object-Oriented Programming.
To find out more about FoxTalk and Pinnacle Publishing, visit their website at http://www.pinpub.com/html/main.isx?sub=57
Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.
This article is reproduced from the August 2001 issue of FoxTalk. Copyright 2001, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-493-4867 x4209.