"Getters and Setters" - a different CFC approach
With an ongoing debate over me stating that generic getters and setters were a bad idea, I thought I'd take a stab at an implementation that tried to fix Peter Bells's fault with them (they're verbose) without falling into problems I had with Peter's "Generic" approach:
- They're a direct encapsulation violation, as you have to know implementation details of the CFC to know how to use it.
- It's based on the magic string / magic number antipattern, where strings evaluated at runtime take on API, not data, significance.
- They don't produce an API that can be used by others, as an object has no external API describing its properties.
While I was accused of zealotry, the accusation was based on the assumption that I thought forcing people to write getFoo() and setFoo() is a hard-and-fast rule applicable to every application.
It's not.
I cooked up the following bit, following an idea Mark Mandel had a while back while discussing implicit setters. I can't claim any originality - over IM last night, Mark knew exactly what I was up to before I even told him what I was doing.
The "hack" results in a property manipulation API such as this, where you use <cfproperty> tags to declare properties, and only those marked as access="public" can have accessors and writers "generated" on-the-fly:
<cfcomponent extends="AccessorHack">
<cfproperty access="public" name="name" />
<cfproperty access="private" name"privateProperty" />
</cfcomponent>
<!--- Shows "Hank" --->
<cfoutput>#contact.name()#</cfoutput>
<!--- Tosses private property error --->
<cfset contact.privateProperty("value") />
<!--- Tosses variable not defined --->
<cfreturn contact.undefinedProperty() />
What I Like About It
- Objects are still true objects - they have defined properties. Encapsulation and API definition are maintained.
- Instead of literals (set("Name")), you work against methods.
What I don't like about it
- The syntax is a bit weird. The equals sign is just what I see as being assignment to a property, but setFoo() doesn't have that either.
- There's a runtime cost associated with getting all properties up an object's inheritance chain on the first get/set, and I'd find that unacceptable in a high-load app.
- There's no type-checking, as I didn't want to add that cost or write the code for it.
Conclusion
It's an interesting experiment, but not one I'll put forth as a good technique for big apps.
Code
Here's the code that was used (obviously CF8-only):
<!--- Builds a table of properties metadata that crawls up inheritance chain. --->
<cffunction name="buildPropertyTable" output="false">
<cfargument name="table" default="#structNew()#" />
<cfargument name="md" default="#getMetadata(this)#" />
<cfset var prop = 0 />
<cfif structKeyExists(md, "properties")>
<cfloop array="#md.properties#" index="prop">
<cfif structKeyExists(prop, "access") and prop.access eq "public">
<cfset table[prop.name] = prop />
</cfif>
</cfloop>
</cfif>
<cfif structKeyExists(md, "extends")>
<cfset buildPropertyTable(table, getComponentMetadata(md.extends.name)) />
</cfif>
<cfreturn table />
</cffunction>
<!--- Sets / gets whatever is passed directly to / from variables, as long as it's declared "public" --->
<cffunction name="onMissingMethod" output="false">
<cfargument name="missingMethodName" />
<cfargument name="missingMethodArguments" />
<!--- Build table if necessary --->
<cfif not structKeyExists(this, "publicPropertyMetadata")>
<cfset this.publicPropertyMetadata = buildPropertyTable() />
</cfif>
<cfif not structKeyExists(this.publicPropertyMetadata, missingMethodName)>
<cfthrow message="#missingMethodName# is not declared as public in a <cfproperty> tag!" />
</cfif>
<!--- Beware: doesn't support named args --->
<cfif structCount(missingMethodArguments)>
<cfset variables[missingMethodName] = missingMethodArguments[1] />
<cfelse>
<cfreturn variables[missingMethodName] />
</cfif>
</cffunction>
</cfcomponent>
5 comments - Posted by Joe Rinehart at 8:00 AM - Categories: ColdFusion MX | OOP | Causing Trouble