Ask Ben: When To Provide Default Values In ColdFusion
Having read "Object-Oriented Programming in ColdFusion" by Matt Gifford, I'm excitedly getting into creating components. With lots of functions. So as I was coding, I started thinking about CFFunction and CFArgument, and came up with a question I couldn't answer: Generally, when supplying an optional argument, I provide a default value. But is there a difference between declaring an argument required, and declaring it not required but failing to declare a default? If not, why should I take one approach or the other?
In ColdFusion, there are two primary opportunities for default value creation: CFArgument and CFParam. I've thought about this question for a while now, and the only practical argument that I can make for or against creating default values is: Does it make your life easier? If it does, create a default value; if it doesn't, don't.
In the vast majority of situations, it has been my personal experience that creating a default value serves to make my code much more straightforward, readable, and maintainable. By using default values, my validation logic is typically smaller and more Boolean-based. Also, it gives me more of an opportunity to perform in-page validation and error handling.
In fact, the only time that I can think of in which a default value causes problems is when the default value itself carries too much functional significance. That is, when there is a meaningful difference between "no value" and a default value. Such a situation might be one in which the number of arguments passed to a function significantly alters the way in which that function is processed. If your internal logic is dependent on the arrayLen() of arguments (such as with arraySplice()), providing arguments with default values will ultimately do you a disservice.
When working with non-required CFArguments, however, you just have to be careful about the way in which they present themselves. While a non-required argument value does not exist (and will raise an exception if referenced), the key for that argument "sort of" exists within the Arguments collection.
Should I Just Let The Application Handle Global Exceptions?
If you don't provide a default value for required user-provided parameters, you will, at some point, get exceptions. So, the next logical question in this philosophical conversation is, Should you not worry about default values and just let those exceptions be raised and then subsequently handled by the calling context?
This is a wonderfully provocative question since, at some level, unexpected exceptions are really only raised when the system is being used improperly. So, if someone didn't provide a value that they were supposed to, technically speaking, the application doesn't have to work properly in response. This is an issue that I have been contemplating for years.
Once again, I think it comes back to, Does the default value make your life easier? In the case of a required URL paramater, I would definitely argue that not providing a default value makes your life easier. After all, why try to recover from a critical mistake? Since you don't have all the necessary information, letting the exception bubble up to the global error handler is completely logical.
Having said that, however, I have to admit that I often provide default values (using CFParam) for critical URL variables. I do this out of habit, though, not as a form of best practice. In fact, re-reading what I just wrote above definitely makes me want to be more judicious about the defaults that I do provide.
With all this back and forth, however, it still seems that everything boils down to a matter of value. If default values provide you with a benefit, use them; if they don't provide you with a benefit, don't use them.
Hmmm, I really hate to leave this entry on a point of, "It depends." In my entire time as a programmer, I have always looked down upon such a response. I feel like it's a teaching tool that holds no educational value. As such, let me leave you with some more concrete, context-sensitive answers:
- Required URL Parameters: Don't provide default values as your system should handle the passing of these values implicitly. If they are not present in the URL, then the request is not valid and should be handled by the global error handler.
- Optional URL Parameters: Do provide default values as it will simplify your processing logic as well as provide some level of self-documentation.
- Required Custom Tag Attributes: Do not provide default values for critical attributes as the developer should be made aware of the problem immediately (as indicated by the runtime exception).
- Optional Custom Tag Attributes: Do provide default values as custom tags often provide many more attributes than are required for possible invocation.
- Required Function Parameters: Don't provide default values as the developer should be made aware of the problem immediately (as indicated by the runtime exception).
- Optional Function Parameters: For this one, the first thing you should probably do is ask yourself why the function has optional parameters. It's definitely possible that you are trying to shove two methods into a single function signature. Perhaps refactoring to two functions can allow all parameters to be required. If, however, you have determined that the optional parameters are required, do provide default values if the default values can be provided. In some cases, such as those in which a function can accept N-number of arguments, no default values can be provided ahead of time.
Now, having re-read the list above, the rule of thumb becomes much more obvious: If the parameter is required, don't provide a default value. If the parameter is optional, do provide a default value whenever possible.
Sweet - sometimes a stream-of-consciousness approach really gets us to the right answer!
Reader Comments
Whoa, cool - I've inspired a kinky blog post!
That's more or less what I've arrived at: If I need to use the value, I need to raise an error if it's not provided. If not, then I can make a reasonable assumption about what it should be if it's not provided.
The question was raised when I was creating an Update method in a DAO component. I didn't know which fields in particular would be updated, but I didn't want to provide defaults for fields that weren't provided in the function call (since any defaults I provided could wind up changing a value that had already been set). How do you deal with a situation like that?
Matt, I can think of a couple of ways of handling that.
One is to require all fields in the component and update all fields in the method - you'll know that all values are valid (assuming you've got a method to preserve nulls), but if you are logging SQL or otherwise tracking changes outside the database, your logs will be significantly bigger for large objects. (Logging within the database may or may not be affected, depending on how you're logging changes - by record or by field.)
Another is to pass (or build) a list of changed fields and build the Update statement from that list. The resulting SQL will be smaller, but there could be a bit of work involved depending on how many fields are involved and how many different types you have. (You may need a couple of helper methods for this, ones that compare different types of values and build SQL fragments to update them accordingly.)
I don't believe the question is if it makes your life easier but if creating a default is logical. It should be consonant with what is occurring. Over the years a common problem I've seen is people create a default value, like empty string, and it leads to odd errors being reported down the road.
If something shouldn't exist, don't force it to exist. Things usually don't exist for a reason. Your method isn't getting used the way you expected it, it's too tightly coupled, etc, etc.
@Matt,
That's a tricky situation. I used to have save() style methods that had optional arguments that were only meant for the "create" version. What I tend to do now is have a create() and update() method and have all arguments required. While this doubles the number of methods, I find that the update() method is typically much smaller and more straightforward. What I lose in a common interface, I think I gain in a smaller signature.
Though, I'm not 100% sold on the idea. It's just what I do currently.
@Allen,
Agreed. I tried to bring it around to that point at the end.
I've been reading "Clean Code" by Robert C. Martin. Great book, highly recommend it (http://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882). One point he makes is that each function should do one thing and not different things based on what arguments come in. So the create() and update() are more clear than the save() with a flag that would direct the function in the DAO to do either.
On the default value subject. I thing it is a little frustrating that when creating a cfscript based component, there's no way to assign default values to array and struct properties. I came up with a work around (i.e. hack) to make it work, but I would like to see this improved in the language at some point.
I've encountered this situation while playing with a proof of concept using restfulCF. Here's the 'work around' in pastebin:
http://pastebin.com/VdJf3fTV
Regards,
Gui Pinto
I use defaults as a form of defensive programming; but mostly because I am soooooo pissed at search bots who dig through javascript code and call the ajax cfcs without any parameters.
@Guilherme,
I think I might actually have that book lying around somewhere; though, I might be confusing it with "Beautiful Code."
I can't remember where I read it, maybe in "Eloquent JavaScript", but someone had a quote about the ideal number of function arguments:
The ideal number of arguments for a function is zero. Next comes one, followed closely by two. Three arguments should be avoided where possible. More than three requires very special justification - and then shouldn't be used anyway.
I actually just found that on Rebecca Murphey's website as someone quoting Clean Code:
http://blog.rebeccamurphey.com/objects-as-arguments-in-javascript-where-do-y
Ha ha, what a small world :) I end up talking about the book you are talking about. Now, I feel like have to get it.
Interesting work-around, by the way. I never thought of looping over the properties to evaluate defaults. Neat idea.
@Nelle,
For real! That's wicked annoying, especially when you are doing something to log errors (since they hopefully shouldn't ever happen).
@Ben,
we're logging them and sending an e-mail to a certain imap folder.
the quote about the ideal number of function arguments reminds me of monty pythons quest for the holy grail and the holy hand granade bit.
@Ben,
I like that quote! But what happens when you're writing an insert function to a table with ten fields? Certainly you can pass a single argument - an object with ten properties, say. But then what about the constructor for that object?
In regards to the original "When To Provide Default Values[...]?" question, my position would be that they should be specified when that default value is the value that one would use for that argument almost all the time. For example in CF's own list functions, the default delimiter is a comma. Most of the time, that's the delimiter one would want to use, so it makes sense to make that the default.
At the very least it should be a "real" value that the function will use.
What one shouldn't do is to - for example - default a string argument to an empty string, and then have logic to ignore it if it's an empty string. Setting a default value that is actually an *exception* rather than a default is counter to the intent of a default value.
As for the comment about the ideal number of arguments a function should take, I think it's a lovely sound-bite, but other than that, doesn't stand up to too much scrutiny. A method should take as many arguments as it needs to perform its functionality. Not fewer. Not more. Just the correct amount, however many that is. Obviously one should consider whether a function is doing too many things (any more than one thing is too many. And the one thing it does should be described by the function name). But that should be based on the logic encapsulated within the function rather than a tally of its arguments. But it is a nice sound bite, anyhow.
--
Adam
@Adam,
I definitely agree with you that a functional default should be the one used most often (as in the case of the comma). But, I am not sure how I feel about the default-as-exception concept. Maybe we are thinking about two different things, though.
When I think of that, I think about maybe having a function that runs a query but provides an optional filter value. Something like:
getPets( [ petTypeID ] ){ ... }
Here, we are getting a collection of pets that may be optionally filtered by petTypeID. The way I would traditionally wire this up would be to have an argument default to zero (assuming none of my property IDs every start at zero):
Then, somewhere in my encapsulated query, I'd have something like:
I could have used something like:
But the default-to-zero just feels much easier to use and to read.
That said, thanks for the sanity-check regarding the number-of-arguments sound byte :) Sometimes, we definitely get lost in the poetry / zen of concepts and fail to see the pragmatic side.
@Matt,
When it comes to create() style functions, I typically pass in the arguments as individual values. Though, I know some people like to pass them in as a single struct. Of course, in ColdFusion, you can kind of merge the two ideas with argumentCollection, which allows us to translate a struct to named arguments during invocation.
Hi Ben
I'm afraid your example is a perfect demonstration as to what I think is wrong with setting a default-as-exception (as you put it).
You're setting a variable, and then only using that value to make sure it's NOT that value before doing [some action with that variable]. This is illogical. A far more logical approach is to not set a default here, and then check to see if the calling code has passed a real value (you kow: one that it actually wants to use), and then if a value has been passed, then use it.
It's no more untidy that your code: simply a structKeyExists() instead of a value-check, and I think the structKeyExists() approach is more in-keeping with the intent of the operation (so it's clearer): if the calling code passes a value in: use it; otherwise: don't.
I'm not trying to change your mind, but I just think your example is a perfect example to describe my position on the topic.
Take it easy mate.
--
Adam
@Adam,
Certainly, I have no technical problem with the approach you are advocating. And, I would agree that using structKeyExist() or isNull() (in CF9+) is more in alignment with what the code is trying to accomplish - if someone doesn't pass in an argument, it shouldn't be there.
I use zero and empty-string defaults because I personally find them easier to work with. Clearly, this is a matter of personal opinion.
When we're dealing with a Function, at least the good news is, it's all encapsulated; we could change the approach internally and the calling context would not be affected. So, at least we've got that going for us :)