"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 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 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 variables.instance.id />
</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('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 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 object.id() EQ this.id() />
</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.)

This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License
Hal Helms speaks, writes, and consults on software development. Hal's popular "Occasional Newsletter" is available at www.halhelms.com.