Happy Medium Between Generic Getters / Setters And Property Methods

Yesterday, I was reading over on Joe Rinehart's blog about his objections to generic Getter and Setter methods. I don't personally use them since I have not yet gotten too much into Object Oriented Programming (OOP), but I thought I would play around this morning with something that would be a happy medium between the two ends of the spectrum. One of the biggest problems with generic getters and setters, as Joe points out, is that there is no provided API and there is poor encapsulation when things don't work as expected. I think these two issues can be addresses using the awesome, dynamic power of ColdFusion that allows you to change the properties of a component at run time (and some good use of the CFThrow tag).

As an experiment, let's take a look at a Person.cfc ColdFusion component. This is a really simple "bean" that has a list of properties and a list of methods that get and set those property values:

<cfcomponent

extends="GenericGetterSetter"

output="false"

hint="Handles methods for a person.">

<!---

Set up data structures and default data values.

These are the values that will be available to the

generic getter and setter if they correspond to spoof

methods below.

--->

<cfset VARIABLES.Instance = {

Gender = "",

Name = "",

NickName = "",

Hair = "",

Eyes = "",

Age = "",

Height = "",

Sexyness = ""

} />

<cffunction

name="Init"

access="public"

returntype="any"

output="false"

hint="Returns an intialized component.">

<!---

Allow the GenericGetterSetter.cfc to configure this

instance for use with the generic getter and setter.

--->

<cfset SUPER.Init( ArgumentCollection = ARGUMENTS ) />

<!--- Return This reference. --->

<cfreturn THIS />

</cffunction>

<!--- BEGIN: spoof functions. --->

<cffunction name="GetGender" kinky:type="spoof"></cffunction>

<cffunction name="SetGender" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<cffunction name="GetName" kinky:type="spoof"></cffunction>

<cffunction name="SetName" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<cffunction name="GetNickName" kinky:type="spoof"></cffunction>

<cffunction name="SetNickName" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<cffunction name="GetHair" kinky:type="spoof"></cffunction>

<cffunction name="SetHair" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<cffunction name="GetEyes" kinky:type="spoof"></cffunction>

<cffunction name="SetEyes" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<cffunction name="GetAge" kinky:type="spoof"></cffunction>

<cffunction name="SetAge" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<cffunction name="GetHeight" kinky:type="spoof"></cffunction>

<cffunction name="SetHeight" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<cffunction name="GetSexyness" kinky:type="spoof"></cffunction>

<cffunction name="SetSexyness" kinky:type="spoof">

<cfargument name="Value" />

</cffunction>

<!--- END: spoof functions. --->

</cfcomponent>

Notice that this ColdFusion component has a list of internal property values; this is pretty standard. But look below the Init() method - there is a list of getter and setter method for these internal properties. These getter and setter methods have no content; they are there merely to provide a footprint for the component API. To flag that these are, indeed, footprint methods and not real methods, I am including a namespace-attribute, kinky:type, and setting its value to "spoof". Below, you will see that this will allow the ColdFusion component to be reconfigured at run time.

Now, if we get the component meta data, you can see that this provides a nice API footprint:

<!--- Output component meta data. --->

<cfdump

var="#GetComponentMetaData( 'Person' )#"

label="Person.cfc Component Footprint"

/>

When CFDumping out the component meta data, we get:

Notice that all of our spoof functions are available for viewing and therefore can be used for documentation creation. I think, so far, this is a pretty happy medium between Joe's explicit property methods and Peter Bell's generic getters and setters. Yes, you have to provide the property methods, but they don't have to have any content! This means very little overhead in terms of typing.

So, that's the compile time / documentation foot print of the ColdFusion component. And, in fact, that is how it works at run time as well. Let's create an instance and set and get several of its properties:

<!--- Create person instance. --->

<cfset objDeborah = CreateObject( "component", "Person" ).Init() />

<!--- Set properties. --->

<cfset objDeborah.SetName( "Deborah" ) />

<cfset objDeborah.SetHair( "Black" ) />

<cfset objDeborah.SetHeight( "5'0""" ) />

<cfset objDeborah.SetSexyness( 10 ) />

<!--- Output properties. --->

Name: #objDeborah.GetName()#<br />

Hair: #objDeborah.GetHair()#<br />

Height: #objDeborah.GetHeight()#<br />

Sexyness: #objDeborah.GetSexyness()#<br />

Running the above code, we get the following output:

Name: DeborahHair: BlackHeight: 5'0"Sexyness: 10

Works just as expected, right? Ok, now here's where the magic is going on - let's take a look at the RUN TIME footprint of the component:

<!--- Create person instance. --->

<cfset objDeborah = CreateObject( "component", "Person" ).Init() />

<!--- Output the runtime footprint. --->

<cfdump

var="#objDeborah#"

label="Runtime Person Footprint"

/>

Dumping out the instance at run time gives us this footprint:

Notice that the spoofed getters and setters are no longer there. Now, we just have a Get(), Set(), and OnMissingMethod() method.

So, what's going on here? How do we get from a API-friendly compile time footprint to a generic getter/setter run time footprint? The voodoo black magic is happening in the component extension. If you look at the code above, you will see that Person.cfc extends the GenericGetterSetter.cfc ColdFusion component. This extended component is, at instantiation time, converting the spoofed methods into generic getter and setter methods when they have the proper corresponding instance properties. It then uses the OnMissingMethod() method as a proxy to the generic Get() and Set() methods to allow the developer to use the original Getters and Setters provided in the documentation.

Here is the code for the GenericGetterSetter.cfc ColdFusion component. As you can see, it is essential that the SUPER.Init() method is called from the instantiated CFC as this is where the component cleaning is taking place:

<cfcomponent

output="false"

hint="Provides generic getter and setter methods based on intance properties and spoof methods.">

<!--- Set up data structures and default data values. --->

<!---

Set up the list of properties that can be read and

written using the generic getters and setters. For

now, this will be just a generic list since we want

this to be defined using the method list.

We will maintain a separate list for the getter and

setter properties.

--->

<cfset VARIABLES.PropertyList = {

Get = "",

Set = ""

} />

<cffunction

name="Init"

access="public"

returntype="any"

output="false"

hint="Returns an intialized component.">

<!--- Define the local scope. --->

<cfset var LOCAL = {} />

<!--- Get the Methods for this CFC. --->

<cfset LOCAL.Methods = GetMetaData( THIS ).Functions />

<!---

Loop over the methods looking for the ones that are

flagged with our spoof flag. These are the methods

that should correspond to the property list.

--->

<cfloop

index="LOCAL.Method"

array="#LOCAL.Methods#">

<!--- Check to see if this is a spoof. --->

<cfif (

StructKeyExists( LOCAL.Method, "kinky:type" ) AND

(LOCAL.Method[ "kinky:type" ] EQ "spoof")

)>

<!--- Get the property method (get/set). --->

<cfset LOCAL.PropertyMethod = Left(

LOCAL.Method.Name,

3

) />

<!--- Get the property name. --->

<cfset LOCAL.PropertyName = Right(

LOCAL.Method.Name,

(Len( LOCAL.Method.Name ) - 3)

) />

<!---

Check to see if this property is in the list

of instance properties.

--->

<cfif StructKeyExists( VARIABLES.Instance, LOCAL.PropertyName )>

<!---

This value can be get/set using our

generic getter and setter methods. Add

the property to the valid property list

and then delete the given method.

--->

<cfset VARIABLES.PropertyList[ LOCAL.PropertyMethod ] = ListAppend(

VARIABLES.PropertyList[ LOCAL.PropertyMethod ],

LOCAL.PropertyName

) />

<!--- Delete the spoof method. --->

<cfset StructDelete( THIS, LOCAL.Method.Name ) />

</cfif>

</cfif>

</cfloop>

<!--- Return This reference. --->

<cfreturn THIS />

</cffunction>

<cffunction

name="Get"

access="public"

returntype="any"

output="false"

hint="Generic getter for valid instance properties.">

<!--- Define arguments. --->

<cfargument

name="Property"

type="string"

required="true"

hint="The property name being retreived."

/>

<!---

Check to see if this property can be gotten based on

access permissions.

--->

<cfif ListFindNoCase(

VARIABLES.PropertyList.Get,

ARGUMENTS.Property

)>

<!--- Return property. --->

<cfreturn VARIABLES.Instance[ ARGUMENTS.Property ] />

<cfelse>

<!--- The property was invalid. --->

<cfthrow

type="Instance.InvalidProperty"

message="Invalid property"

detail="The property you requested, #UCase( ARGUMENTS.Property )#, is not a valid property for the generic getter."

/>

</cfif>

</cffunction>

<cffunction

name="Set"

access="public"

returntype="any"

output="false"

hint="Generic setter for valid instance properties.">

<!--- Define arguments. --->

<cfargument

name="Property"

type="string"

required="true"

hint="The property name being set."

/>

<cfargument

name="Value"

type="any"

required="true"

hint="The property value being set."

/>

<!---

Check to see if this property can be set based on

access permissions.

--->

<cfif ListFindNoCase(

VARIABLES.PropertyList.Set,

ARGUMENTS.Property

)>

<!--- Set property. --->

<cfset VARIABLES.Instance[ ARGUMENTS.Property ] = ARGUMENTS.Value />

<!--- Return This reference for chaining. --->

<cfreturn THIS />

<cfelse>

<!--- The property was invalid. --->

<cfthrow

type="Instance.InvalidProperty"

message="Invalid property"

detail="The property you set, #UCase( ARGUMENTS.Property )#, is not a valid property for the generic setter."

/>

</cfif>

</cffunction>

<cffunction

name="OnMissingMethod"

access="public"

returntype="any"

output="false"

hint="Used to proxy the generic getter and setter methods.">

<!--- Define arguments. --->

<cfargument

name="MissingMethodName"

type="string"

required="true"

hint="The name of the requested method."

/>

<cfargument

name="MissingMethodArguments"

type="struct"

required="true"

hint="The struct of arguments."

/>

<!--- Define the local scope. --->

<cfset var LOCAL = {} />

<!--- Get the property method (get/set). --->

<cfset LOCAL.PropertyMethod = Left(

ARGUMENTS.MissingMethodName,

3

) />

<!--- Get the property name. --->

<cfset LOCAL.PropertyName = Right(

ARGUMENTS.MissingMethodName,

(Len( ARGUMENTS.MissingMethodName ) - 3)

) />

<!--- Check to see what the method is. --->

<cfif (LOCAL.PropertyMethod EQ "Get")>

<!--- Return property. --->

<cfreturn THIS.Get( LOCAL.PropertyName ) />

<cfelseif (LOCAL.PropertyMethod EQ "Set")>

<!--- Set property. --->

<cfreturn THIS.Set(

LOCAL.PropertyName,

ARGUMENTS.MissingMethodArguments[ 1 ]

) />

<cfelse>

<!---

If we have made it this far, then we are

accessing an invalid method.

--->

<cfthrow

type="Component.MissingMethod"

message="Method does not exist"

detail="The method you are attempting to access, #UCase( ARGUMENTS.MissingMethodName )#, is not a valid method of this component."

/>

</cfif>

</cffunction>

</cfcomponent>

As the GenericGetterSetter.cfc converts the spoof methods into gettable and settable properties, it builds a list of valid properties that can be used generically. This way, you still get the benefit of making sure the programmer cannot start getting and setting random internal variables. And, since this only converts "spoof" methods, you can still have other utility methods like GetAge() that leverage the DateOfBirth property.

Of course, if you wanted to use other property values in your Getter / Setter methods, you would have to modify the way it checks value (but this would be a really minimal update). But really, this was just a fun idea to play with. I am not really saying that you should do this in any way; I was merely trying to come up with a way to satisfy people who like API-friendly, explicitly methods as well as people who like generic getters and setters... and to do this all with minimal additional overhead.

If your primary concern is the amorphous model, I thought you'd still be unsettled with the use of CFProperty as it has no interaction with the actual implementation of the code. I am not 100% sure, but I am pretty sure you can have complete garbage in the CFProperty list and still have a fully functioning CFC.

I figured by at least having the spoofed CFFunction tags, you have something that is truly "functional" from a programming stand point.

Cool :-> Funnily enough, though, I'd actually side with Joe. If I was putting the metadata in the object in a more traditional way, I'd annotate the cfproperty tags and treat them as the definitive API. I've actually played around recently with an implementation of my IBO which is based on metadata in cfproperty tags - almost exactly what Joe said - I used the more active form: gettable="true/false" settable="true/false".

The two issues I have with this are that it forces me to have physical cfc's for all of my model objects as opposed to just magicking them up from a base object and some metadata, and the fact that it assumes that a static view of the object is appropriate as opposed to a more dynamic state or roles based approach. The second, may or may not be important - haven't thought it all the way through yet. The first is a deal killer for my particular use case but I think either your approach or the annoted cfproperty tags would be cool. To be honest, I think your approach rocks, but I'd probably be more likely to use the cfproperty approach as there is a little less magic going on which can sometimes be a good thing.

This was just for fun... I still play in the procedural world, so this is all just a mental exercise for me :) And if it gets anyone thinking, even better.

Something about the CFProperty tag just rubs me the wrong way (having nothing to do with this situation specifically). Can't quite put my finger on it. When I first learned about CFCs, I used to use it all the time... then I realized that it didn't do anything for my every day programming. I guess it just feel hacky in that its like meta data, not real data. Not exactly the right words. Maybe I am just speaking from ignorance - afterall, I LOVE the "hint" attributes, and they don't really affect programming either. So, I guess it just comes down to what I have trained myself to enjoy.

When it comes down to it, everything is just magic strings. Even a function is just a magic string - it just happens to be one that the interpreter/compiler understands! It's not like we're editing AST's directly - we're all just using a textual representation of our intent.

The difference between a comma delimited list, cfproperty and cffunctions mainly comes down to tooling support. If you use something like JavaDocs, cffuncitons would be best. CF property doesn't have a load of tooling support but it's a supported language feature in CF. A comma delimited list of property names isn't supported by the IDE or the language so if you go that way you have to roll your own tooling and then teach anyone using the software what your tooling is and how to use it.

I have to mostly agree with you. Even the documentation argument is a hard one for me to side with as I rock HomeSite, so its not like I get any CFC insight, and I rarely ever have documentation for anything, no matter how it is coded :) The tooling support is not an issue for me because it's rarely an option anyway.

The biggest point that I can side with is that when I open up a CFC to see how it works, it shouldn't be obfuscated. Of course, as long as you understand the programming practices in place, it shouldn't matter.

Yeah - a blog posting on this is due. The question is whether your ColdFusion code is the definitive source of documentation or not. I find that most of the CFC"s in most of my apps don't actually have any functionality I can't describe in a DSL, so I don't even HAVE files for most of my classes. There are big benefits to the approach, but you have to make alternative arrangements for things like documentation as the code becomes "the bits and pieces you couldn't describe in a DSL" rather than "the definitive reference to what your application does".

@Ben, Strictly speaking the DSLs describe things like the business objects rather than the class structure (so there is a description of the Product business object but no description of the implementation decision to have a Product.cfc, ProductDAO.cfc and ProductService.cfc), but yep - the DSLs basically provide the documentation and just pull in the methods from the class files as the "custom methods".

"Yes, you have to provide the property methods, but they don't have to have any content! This means very little overhead in terms of typing."

Hmmm... I'm really wondering on this one. Because really, how much content do you have in a getter/setter. If you're gonna type anything at all for the getter/setter, typing the extra <cfreturn> or <cfset> probably isn't gonna be much more. Using snippets you can have most of the getter/setter done, the rest is fill-in-the-blanks. Plus if you're using an editor like TextMate (or E-TextEditor on Windows), you can just have a snippet that will automatically fill in the name as you type it in 3 different places (very cool feature!), in which case doing your getter/setter amounts to typing some trigger text, hitting tab, and typing the name of the property to get/set. In the end, I think one of Peter's stated issues is also that he'd rather avoid having a bunch of extra methods bloating a CFC if they're needed universally and you can come up with something generic to eliminate them.

Having said that, I still think what you've come up with is pretty ingenious and nifty, that's what keeps us all coming back for more!

The dump immediately after the GetMetaData (in A.cfc) is complete, the dump in B.cfm only shows a struct with 2 keys: 'initiated' and 'methods' whch is a non-complete listing of all methods in the inheritance chain.Any idea why the struct that gets returned from the cfc is different from the struct that is output within the cfc? Maybe security...

You make a good point. I was hoping that something like this would work:

<cffunction name="GetName" />

... would work, but I didn't even bother trying it. I felt that if I went that way with the Set methods anyway, you would lose the insight of the argument.

But, it is true, once you have those tags, its really not much more to just write the entire functions.

@Marc,

Dumping the meta data and dumping the component itself are getting two different flavors of information. The meta data is the information about the CFC and the object dump is the object itself at run time. There is some overlap in the information that is reported, but ultimately, these will not be the same.

Think about a query - when you dump out the meta data of the query, you get something extremely different than when you dump out the query itself.

Oops...<cfset t=CreateObject("component","A">should be<cfset t=CreateObject("component","A").init()>Now the cfdump in the component and the cfdump from the returned struct are identical.Btw If I place this:<cfset THIS.init()> in the component before the init() method (to get it executed automatically on instantiation as a constructor) the 2nd dump suddenly is a dump of the methods in the cfcs - as before, not of the metadata.Marc

I actually posted something similar to Joe's original comment in Peter's blog posting on this subject. I liked the idea of declaring the properties in the cfproperty tags, and decorating them with some metadata. This would not only allow you to define whether a getter/setter should be allowed, but also some additional validation rules (ie. length for a string property, type, etc.).

This approach would cut down on the amount of typing (a single line for the cfproperty tag), and would prevent you from having to explicitly declare the instance variables (these could be generated in your init function, by iterating through the properties and creating instance variables based on the default attributes of the property).

@Allen - well if you design your objects to take advantage of the metadata of the properties and use that *instead of* writing the individual methods, as Joe aluded to in his first comment, then what you get actually is a very DRY object where you have just the properties and any necessary behavior methods. FarCry did this and I added support for it in a more recent version of DataFaucet also. There's a blog entry about that here: http://datafaucet.riaforge.org/blog/index.cfm/2008/10/14/BestOfBothWorlds

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.