Synthesized Objects
Posted by
Brad Wood
Sep 20, 2008 18:12:00 UTC
The other day I was writing a wrapper CFC to consume a web service, and return the results. Several of the web services' method returned arrays of structs and I was in the mood to experiment, so I decided to have my CFC present the data back as an array of components. I knew that there would really never be more than 20 or 30 objects coming back so the cost of object instantiation wouldn't be a big deal. There were 7 different "types" of objects coming back in the various method containing mostly strings and an occasional binary image. I didn't want to do all the typing so I decided to toy with Object Synthesization similar to what Peter Bell has been talking about.I wanted to create each object with minimal code, and I wanted all the properties to be private (variables scope). I also wanted to be able to call a getter to retrieve the data. By the time I was finished, I ended up with a base class called bean.cfc which actually had everything in it:
[code]<cfcomponent name="bean" hint="I am the base class for a bean"> <cffunction name="init" returntype="any"> <cfset structappend(variables,arguments)> <cfreturn this> </cffunction> <cffunction name="OnMissingMethod" access="public" returntype="any" output="false"> <cfargument name="MissingMethodName" type="string" required="true"> <cfargument name="MissingMethodArguments" type="struct" required="true"> <cfif left(arguments.MissingMethodName,4) eq "get_" and structkeyexists(variables,right(arguments.MissingMethodName,len(arguments.MissingMethodName)-4))> <cfreturn variables[right(arguments.MissingMethodName,len(arguments.MissingMethodName)-4)]> <cfelseif left(arguments.MissingMethodName,4) eq "set_" and structkeyexists(variables,right(arguments.MissingMethodName,len(arguments.MissingMethodName)-4))> <cfset variables[right(arguments.MissingMethodName,len(arguments.MissingMethodName)-4)] = MissingMethodArguments[1]> <cfelse> <cfthrow message="No Such Method Name" detail="A method named #arguments.MissingMethodName# was not found in this Component"> </cfif> </cffunction> </cfcomponent>[/code]My init method simply takes the argument collection and shoves it into the variables scope. This basically assumes that ALL properties are private and all the arguments coming in map to a property of the same name. Lastly, my onmissingmethod handles all getters and setters. Once again, this assumes they are all named "get_" or "set_" followed by the name of the property, and that all properties are stored in the variables scope and can be served up directly. A custom getter or setter could be defined to "override" the onmissingmethod, but I didn't need to in my case. Next, since I felt bad about directly instantiating my bean.cfc since it felt like a sort of abstract class, I created 7 mostly empty CFCs for each object named appropriately. Here is one called client_image.cfc:
[code]<cfcomponent name="client_image" extends="bean" hint="I am an image of an e-mail in a specified client."> <cffunction name="init" returntype="any"> <cfreturn super.init(argumentcollection=arguments)> </cffunction> </cfcomponent> [/code]You can see all it does is extend the base bean, and call the super's init passing along the argument collection. I would create my objects like this:
[code]<cfset local.tmp_object = createobject("component","client_image").init(argumentcollection=local.this_client)>[/code]All in all it was an interesting experiment, but I can't say I like it. Firstly, there were no OO warm fuzzies washing over me. Instead it felt like I had just taken a struct and placed a CFC "coat" over it. Secondly, if I was going through so much trouble NOT to write code, I might as well have skipped the child classes and directly instantiated my bean class since I didn't override anything. Thirdly, my bean was pretty dumb. And by dumb I mean not smart. And by not smart I mean the classes didn't self-document anything, didn't validation anything, didn't require anything. They just sat there and accepted whatever you shoved into them and coughed it back up when you asked. In the end I think I might as well have just returned the array of structs. On a related note, can anyone tell me if there is an official name for the pattern (or anti-pattern) of using the dynamic getter/setter and a dynamic init like that?
John Whish
Hi Brad, I quite like your idea of passing the arguments collection to the init. If you're insterested I did attempt a similar base class that also validated the data types. I posted it here: http://www.aliaspooryorik.com/blog/index.cfm/e/posts.details/post/cfproperty-onmissingmethod-and-data-type-validation-140.
Brad Wood
@John: Thanks. Using structappend in your init to set all the instance variables is fast and very little coding, but I don't know if I have sold myself on it yet. :) In addition to precluding the possibility of a custom setter, it doesn't enforce any validation. I like what you did in your blog using the properties. That is kind of cool.
One of my struggles with validation is that it seems most useful during development when you are likley to make coding errors, or when someone other than you is going to be using your components. Specifically a third party you cannot trust (such as a web service). It seems that a large number of people who write and use their own components are already aware of the requirements and validation would be more of an extra step-- hence CF 8's setting to turn off data type checking. To me personally, speed and simple code is almost more important than incredibly verbose data checking and explicitly defined accessors. It more RAD at least...
John Whish
@Brad, I agree with you that validation is generally only required in development, but I also think that it is very useful with developing! I did mention in my post that you could easily remove the type checking (and even the variable exists check) from production code for performance. As it is a base class that is reusable there is no extra development time and you get the best of both worlds :)