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
ObjectArrayclass will ensure that all elements within the array are ColdFusion components (CFCs). Let's look at some of the code for
<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>
addfunction 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
addfunction. 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
ObjectArrayclass. To make this happen, use the
extendsproperty of the
<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
objectsarray in your
variables.instancestructure, yet you used this array in your
initmethod. You need not create the
TypedObjectArrayinherited this variable from the
Notice, too, that there are two versions of the
addmethod: one in
TypedObjectArrayand 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
superkeyword. So, within the
TypedObjectArray, you can call
super.add, causing the
ObjectArrayto 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
extendsproperty) implicitly extend
component.cfc. It is the Ultimate Superclass. (Try calling
IsInstanceOfon any object, using the
component.cfcas 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
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
<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) />
o2point to the same object. Since the
UniqueObjectArrayshould ensure that all elements within the array are unique, you'll need to change the
addmethod. 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
<cfset variables.instance.id = 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
idoutside 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 variables.instance.id /> </cffunction>
UniqueObjectArraycan have this code in it...
<cfif anArray.getId() EQ arg.getId()> <cfset ArrayAppend(variables.instance.objects, arg) /> </cfif>
For every instance variable,
id), you can provide
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
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
adobewill have unique IDs. But surely, even though these two references (
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
setXtechnique, I prefer a single
xmethod that can do double-duty as either a "getter" or a "setter":
<cffunction name="id" access="public"> <cfargument name="id" required="false" /> <cfif IsDefined('arguments.id')> <cfset variables.instance.id = arguments.id /> <cfelse> <cfreturn variables.instance.id /> </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
Customerobjects, 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
<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 object.id() EQ this.id() /> </cffunction>
isEqualmethod just returns the results of comparing the
ids, but I've also specified that all objects should have an
isEqualmethod 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
component.cfc. In fact, it's a good idea to always override it." And now, your code for
Customer.cfchas 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.cfccan 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
propsmethod, 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,
<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
init()returns the object initialized with a no-arguments constructor and a new, random UUID.
init(id)returns the object initialized with its
idset to the value of
idpassed 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. The exception to this is the
idfield, 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
idto be used.
mixin(mixinCode)allows for inserting methods to an object at run time.
metadata()returns the call to
These are some of the more commonly-used methods. I also made use of the
OnMissingMethodmethod, 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
setXmethod (when those are necessary) and to detect and call these automatically from within
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.)