Pardon the bad pun, but I wanted to continue the nice, civilized discussion we’ve been having today about OO getters and setters in CF. Ben Nadel started it all, I ran off my mouth, then Brian Kotek took the ball and ran with it. I’m taking the ball now, as I don’t want Brian to think I am picking on him — so come into my house and pick on me for a while.
My assertion, in a nutshell:
The getter paradigm for instance variables is useless in ColdFusion for all but a few, albeit important, edge cases. The proliferation of one-line getters in CF code is just silly — YAGNI. Please think before you click that button to generate a list of getters for every public property in your object.
I’m going to try to stop using contrived examples and start giving concrete ones. Unfortunately, this means I have to find actual code to support my crazy assertions. So, for the record, I haven’t picked this code out to pick on the author — it’s literally just what came to mind.
That being said, I’m going to use a popular piece of Open Source CF: Ray Camden’s BlogCFC. In the eponymous blog.cfc file there are two great examples, right next to each other:
<cffunction name="getProperties" access="public" returnType="struct" output="false"> <cfreturn duplicate(instance)> </cffunction>
This is, IMHO, a great use for a getter — you want to return a complex datatype (struct), so you deep copy it. This getter is actually doing something.
But! I may not mean what you think I mean! Are you copying it for the right reason?
One right reason to make a deep copy might be because you want to provide a stable snapshot of the object state. If you just passed a top-level reference, the reentrant nature of CF might accidentally screw over the caller.
But if you’re copying it in some attempt to keep the caller from mucking about in your internals … why? We’re talking about ColdFusion here. We’re not talking about a language with airtight concepts of public and private data. If the caller wants to tromp all over your private data, it’s going to take finer coding acrobatics than that to stop them.
And again, why? Don’t you trust your caller to behave responsibly? As it stands, this particular coding practice as an attempt at data protection is akin to a “no girls allowed” sign on a treehouse.
The very next function in that file:
<cffunction name="getProperty" access="public" returnType="any" output="false"> <cfargument name="property" type="string" required="true"> <cfif not structKeyExists(instance,arguments.property)> <cfset variables.utils.throw("#arguments.property# is not a valid property.")> </cfif> <cfreturn instance[arguments.property]> </cffunction>
This is exactly the type of thing I was talking about in my original post. What actual purpose does this code serve? How is it any better than just having exposed properties, such as #blog.whatever#?
Does it provide data protection? Eh, not really. In some other language it might, but not in CF. (And again — who are you so afraid of?)
Does it provide safety? The check for existence seems to imply that it might … but that’s a red herring. Now, instead of manually checking for existence, the caller has to check for a thrown error. That’s displacing the problem, not eliminating it. (And is the API so volatile that only parts of it exist at any given time?)
Does it provide some other kind of value-add? Not really. There’s no logging or reference counting or any other kind of magic going on here. You might argue: “but what if I want to add logging or reference counting or other magic later?”. If you don’t have a need to do that now, do you really think that you’ll have a need to do that later? Really? You’re going to log variable reads?
To be completely fair, I can think of a reason why you might want to have this function: if you think that you might, at some future time, change how you are storing your instance variables. Or maybe make an instance variable dynamic. (But is it still then an instance variable?) But again I say: really? Really? How often do you actually change how you store your own properties? Obviously, I don’t think too highly of this reasoning.
More typical examples come later:
<cffunction name="getValidDBTypes" access="public" returnType="string" output="false" hint="Returns the valid database types."> <cfreturn variables.validDBTypes> </cffunction> <cffunction name="getVersion" access="remote" returnType="string" output="false" hint="Returns the version of the blog."> <cfreturn variables.version> </cffunction>
I ask you — is that really any better than simply #blog.version# and #blog.validDBTypes#? How? Is the version returned going to be different based on the caller or some other runtime feature? Is the list of supported database types going to change based on current conditions? At runtime?
The argument here is this: “what if I come back later and do really have some goofy reason for one of these properties being dynamic?”.
Instead of giving you my reason, I’m going to work backwards, trying not to step on any contrived examples.
In order for that data member to suddenly need to be dynamic, it has to depend on something at runtime, right? We can divide the possible ties into two categories: the member is tied to something about the caller, or the member is tied to something about the app state. That is, the influence is external or internal.
In the first case, the member is tied to the caller, the only way this can happen is if the caller is now informing the app about the tie. That is, you’ve gone from #blog.getFoo()# with no arguments, to #blog.getFoo(bar)# with some arguments. Congratulations, you’ve just changed your API and nullified your argument about making transparent changes. Functionally, that’s no different than supplementing #blog.foo# with #blog.getFoo(bar)#.
In the second case you have the member suddenly being tied to something in the app state. This is trickier without a contrived example, but suffice it to say that callers written before the member was dynamic probably won’t know how to deal with a member that that is suddenly dynamic, no matter how they access it. How do you know that they aren’t calling it once and caching it? How do you know what will happen if it changes between two calls from the same caller?
In either scenario, you are breaking the API contract. Whether explicit, by adding new arguments to a function, or implicit, by changing the behavior of a function, the API has still been broken. At this point, the gods of encapsulation weep for you … but they aren’t going to fix the broken calling code for you.
You can stomp your feet all you want that callers that didn’t use the API exactly how you expected them to (result caching or not, for example), but unless your wording in your API documentation was airtight and every client was perfectly coded … the time and effort savings you thought you were making by using a getter are all for nothing. You’re back to square one where someone is fixing their code.
API immutability is a pipe dream. Even frameworks know this. Instead of banging your head against the wall, accept this fact up front. Bolt on additions to the API as they come, and just try to be smart about it. When it gets too ugly to bear, increment the major version number and start with a clean slate — it’s the same no matter how you develop: procedural, OO, functional, or whatever.
So there you go. That’s my point. As a design pattern, getters just don’t make sense in the CF world for everyday apps. Getters are for frameworks and code that is going to be extended and reused a dozen or hundred times over.
Belated thanks go to Ray Camden for putting the code out there in the first place. Again, I’m certainly not picking on Ray, as BlogCFC is a fine hunk of code — I had to hunt around for examples!