User level


12 May 2008
"Chances are if your parents didn't have children, you won't either." –Dick Cavett.
One of the important ideas behind Object-Oriented (OO) development is the notion of inheritance. If class AA is a specialized type of class A, most OO languages allow class AA (the subclass) to inherit methods and properties from class A (the superclass). Adobe ColdFusion is no exception. Let's look at an example of two classes that are closely related.
ObjectArray.cfc is a ColdFusion component (or class) intended to hold objects and to provide methods for those objects. Of course, you could just place objects in ColdFusion's native array, but an ObjectArray class will ensure that all elements within the array are ColdFusion components (CFCs). Let's look at some of the code for ObjectArray.cfc:
<cfcomponent displayname="ObjectArray" output="false" hint="I am an array constrained to objects"> <cfset variables.instance.objects = ArrayNew(1) /> <cffunction name="add" access="public" output="false"> <cfargument name="arg" required="true"> <!--- if arg is object--> <cfif IsObject(arg)> <cfset ArrayAppend(variables.instance.objects, arg) /> <cfreturn /> <cfelseif IsArray(arg)> <cfloop array="#arg#" index="anArg"> <cfset add(anArg) /> </cfloop> <cfelse> <cfreturn /> </cfif> </cffunction> ... </cfcomponent>
The add function accepts either a single object or an array of objects. Some other OO languages—Java, for instance—allow for overloaded methods. These are methods that have the same name, but accept different arguments (different in such a way that the runtime engine can know which version of the method to call). I've simulated an overloaded method for the add function. In Java, that same functionality would be implemented with separate methods:
public Object add(Object object); public Object add(ArrayList array);
So, ObjectArray.cfc is the main class. But what if you want to have another class with the same functionality, but wish to ensure that only objects of a certain type are admitted? This is a perfect case for inheritance: TypedObjectArray.cfc is no more than a more specific version of ObjectArray.cfc. As such, it should have access to the methods and properties in the ObjectArray class. To make this happen, use the extends property of the cfcomponent tag:
<cfcomponent displayname="TypedObjectArray" extends="ObjectArray" output="false" hint="I am an array constrained to objects of a specified type"> <cfset variables.instance.typeName = "" /> <cffunction name="init" access="public" output="false" hint="I am a constructor for this object"> <cfargument name = "typeName" required="true" /> <cfset variables.instance.typeName = typeName /> </cffunction> <cffunction name="add" access="public" output="false"> <cfargument name="arg" required="true" /> <!--- is arg is object ---> <cfif IsObject(arg)> <cfif IsInstanceOf(arg, variables.instance.typeName)> <cfset super.add(arg) /> <cfreturn /> </cfelse> <cfthrow type="IncorrectType" /> </cfif> <cfelseif IsArray(arg)> <cfloop array="#arg#" index="anArg"> <cfset add(anArg) /> </cfloop> <cfreturn /> </cfif> </cffunction> ... </cfcomponent>
Notice that I did not create the objects array in your variables.instance structure, yet you used this array in your init method. You need not create the objects array; the TypedObjectArray inherited this variable from the ObjectArray class.
Notice, too, that there are two versions of the add method: one in TypedObjectArray and one in ObjectArray. This is not an overloaded method, but an overridden method: in the case where a subclass implements the same method, the subclass overrides its parent and the method in the child class will be called.
What, though if, from the subclass, you wish to call the overridden method in your superclass? ColdFusion allows you to do that with the use of the super keyword. So, within the add method of TypedObjectArray, you can call super.add, causing the add method of ObjectArray to be called.
Inheritance is certainly useful—so useful, in fact, that it's greatly overused. Programmers new to OO delight in the code reuse aspect of inheritance chains (where class A extends class B that, in turn, extends class C...and so on). But inheritance breaks what is perhaps the single most important idea in OO: encapsulation. Encapsulation is meant to rein in complexity (arguably the chief enemy of software quality) by providing a well-defined Application Programming Interface (API) that defines the interactions with a class while keeping implementation details private. By exposing the implementation details of a superclass by inheritance, encapsulation is compromised. Inheritance should only be used where the inheriting class truly is the same type, but a more specific version of, its parent.
That said, there is a very good place for inheritance that you may not have thought of. If you examine the contents of the folder webroot \WEB-INF\cftags, you'll find a CFC: component.cfc. All components that do not explicitly extend another class (through the extends property) implicitly extend component.cfc. It is the Ultimate Superclass. (Try calling IsInstanceOf on any object, using the component.cfc as its type and you'll be convinced.)
So, what methods does the component have? (Drumroll, please...)
None. But I'm about to change that. Let's take a look at another subclass of ArrayObject: the UniqueArrayObject. This class is meant to ensure that the same object is not placed in the array more than once. To illustrate, this code should not accept the second call to add:
<cfset arr = CreateObject('component', 'UniqueObjectArray") /> <cfset o1 = CreateObject('component', 'GreatSoftwareDevelopmentHouse').init('CityMind Group, LLC', '941.716.6909') /> <cfset o2 = o1 /> <cfset arr.add(o1) /> <cfset arr.add(o2) />
Both o1 and o2 point to the same object. Since the UniqueObjectArray should ensure that all elements within the array are unique, you'll need to change the add method. The code should be simple enough—something like the following:
<cffunction name="add"...> <cfargument name="arg" required="true" /> <cfloop array="#variables.instance.objects#" index="anObject"> <cfif anObject EQ arg> <cfthrow type="DuplicateObject" /> <cfelse> <cfset ArrayAppend(variables.instance.objects, arg) /> </cfif> </cfloop> </cffunction>
If you try this code, you'll get an error—but it won't be of type DuplicateObject. Instead, you'll find that you can't compare two complex variables with each other. To test for equality of two objects, you need a way of providing objects with their own, unique identity. A UUID would be a good solution. Many languages have the concept of object identity; ColdFusion does not, but you can add this in quite easily by modifying component.cfc.
<cfset = CreateUUID() />
Placing that code outside any functions ensures that it is executed when the object is first instantiated. But how do you get to that id outside of the object itself? If you think of objects not so much as stores for data, but as a collection of services described by a published API, you'll see that the answer is to provide a method to get to that code. Now, it's just here that many new OO developers fall into the mistake of dressing up procedural code in fancy object attire. Welcome to the "getter", as in...
<cffunction name="getId" ...> <cfreturn /> </cffunction>
Now, the UniqueObjectArray can have this code in it...
<cfif anArray.getId() EQ arg.getId()> <cfset ArrayAppend(variables.instance.objects, arg) /> </cfif>
For every instance variable, x(such as id), you can provide getX() and a setX(x) methods. Problem solved.
Well, not really. This checks to see if two references occupy the same memory space—whether, in other words, they point to the same object on the heap—but in many cases, that's not rigorous enough a check. Consider this code...
<cfset o1 = CreateObject('component', 'Customer').init('123456') />
That code creates a new customer object with a customerID of 123456. You might add this to the UniqueObjectArray. But now, you come across this code:
<cfset adobe = CreateObject('component', 'Customer').init('123456') />
Both of the objects referenced as o1 and adobe will have unique IDs. But surely, even though these two references (o1 and adobe) don't point to the same memory space, you don't want them both in your unique object array!
The solution, as it often is with OO programming, lies in creating an API that will serve the users of the object well. In this case, you need to do something other than comparing UUIDs. But do youever need to know the ID of an object? Absolutely, as when you persist the object to a database, where the ID may be used as a primary key value. In those cases, you should provide the functionality of a "getId". Instead of the getX/setX technique, I prefer a single x method that can do double-duty as either a "getter" or a "setter":
<cffunction name="id" access="public"> <cfargument name="id" required="false" /> <cfif IsDefined('')> <cfset = /> <cfelse> <cfreturn /> </cfif> </cffunction>
Do you need to place this code in every CFC you write? Not if you make use of the fact that all CFCs ultimately extend component.cfc. You can place the code in the base component and let all your CFCs inherit from it.
But what about those Customer objects, for which id() isn't sufficient? Because an object's identity is often not the determining factor in whether it "is equal" to another object, I place another method in component.cfc:
<cffunction name="isEqual" hint="You are encouraged to override this method so that it returns a meaningful result for your class"...> <cfargument name="object" /> <cfreturn EQ /> </cffunction>
Yes, the isEqual method just returns the results of comparing the ids, but I've also specified that all objects should have an isEqual method that accepts another object and returns a boolean value. You can't possibly know what it means for an object of any one class to be equal to another object; that must be deferred to the component writers themselves. But the hint conveys your intentions. So you gather your team together and say, "Look, if you ever need to be able to compare two objects to see if theyreally refer to the same entity, override the isEqual method in component.cfc. In fact, it's a good idea to always override it." And now, your code for Customer.cfc has the overriding method:
<cffunction name="isEqual"...> <cfargument name="object" ... /> <cfreturn object.customerId() EQ this.customerId() /> </cfunction>
It turns out that "filling the void" of your empty component.cfc can provide you with a great deal of functionality, very easily. For example, have you ever been frustrated that <cfdump>ing an object does not reveal the values of instance variables for that object? I was too—so I created a props method, which does just that:
<cffunction name="props" output="true"> <cfdump var="#variables.instance#" label="Object Instance Variables" /> </cffunction>
Now, I can quickly, easily see what values my object's instance variables have.
<cfset myCustomer = CustomerDao.load('123456') /> <cfset myCustomer.props() />
If I want to see both properties and methods, component.cfc has an inspect method:
<cffunction name="inspect" access="public" output="true"> <cfset var str = StructNew() /> <cfset str.metadata = getMetaData(this) /> <cfset str.variables = variables.instance /> <cfdump var="#str#" label="Object Inspection Results"> </cffunction>
Here are some of the other methods I have in component.cfc:
  • init() returns the object initialized with a no-arguments constructor and a new, random UUID.
  • init(id) returns the object initialized with its id set to the value of id passed to it.
  • className() returns the name of the object's class.
  • mock() returns an object with each of the instance variables set to the name of the instance variable—so, for instance, an instance variable of homeAddress would hold HOMEADDRESS. The exception to this is the id field, which gives a unique UUID. Both this method and its sibling below are very useful for testing.
  • mock(id) does the same as the above, but allows the user to provide a specific id to be used.
  • mixin(mixinCode) allows for inserting methods to an object at run time.
  • metadata() returns the call to getMetadata(this).
These are some of the more commonly-used methods. I also made use of the OnMissingMethod method, available in ColdFusion 8, and which eliminates the need for 99% of all getters and setters, while still allowing the programmer to provide a specific getX/setX method (when those are necessary) and to detect and call these automatically from within OnMissingMethod.
Sometimes I hear that "ColdFusion isn't really object-oriented"—by which the speaker means: "ColdFusion doesn't do OO exactly like [insert favorite language here]." The claim is based on ignorance of ColdFusion's rich, powerful OO features. I'm a Sun-certified Java programmer; I know C#; I've studied and worked with Ruby; I programmed for years in Smalltalk. I don't work for Adobe. If there were a more productive language to program in, I would use it.
When you hear programmers disparage ColdFusion,you should smile. (They are, after all, our competitors.)