Should Optional Method Parameters Be Defaulted To Blank, Or Left Undefined?
Posted by
Brad Wood
May 31, 2009 05:51:00 UTC
Today I offer you a question of preference. Should optional method parameters be defaulted to blank and assumed to exist, or should they only be defined if you intend to use them?
In the name of code reusability I will find myself writing generic service methods such as userService.loadUser() which have a large list of optional parameters such as lastName, firstName, userName, companyID, userID, password etc. I will reuse this method any time I want to load a user or a list of users. The question is this: is it more correct to default all of the arguments or check for their existence?Usually, the scenario has a few layers to it. I call the userService, which calls upon my object factory to create a new user. Then I pass along the parameters to a loadUser() method in my new user object which, in turn, requests the data to populate itself from its DAO (passing the arguments along). I do not relish the layers of relatively empty methods that get called before the actual query is ran, but I like my services to create all my transients and I prefer the encapsulation of my beans populating themselves with their own DAO. I guess that configuration is another topic though.
Luckily CF makes it fairly easy to handle any number of arguments along the way with the argumentcollection magic. The question really is when I get ready to run the query, is it better to assume all the parameters are defined or not. In Java, all variables must be defined at compile time, but they can have a NULL value. In Adobe ColdFusion, NULL doesn't exist, so I have always considered it better form to simply not create variables I have no use for. This also eliminates the problem of trying to differentiate between a parameter which should be ignored and a parameter which is a legitimate empty string.
Of course, occasionally I will have the need to insert, update, or compare an actual NULL value in the database. One can run into the same problem of deciding how to represent that NULL value as a parameter to your DAO. Empty String? The text "NULL"? Omitting the parameter all together?
One final problem, is making sure a generic select with a dynamic where clause based upon the inputs doesn't accidentally return the entire contents of your table if no parameters or all-empty parameters were to be passed in. It feels a little tacky, but I will usually make a specific flag to force all records to be returned in case I need it.
Here is a quick example of a DAO method that assumes all values will exist.
[code]<cffunction name="loadUser"> <cfargument name="firstName" required="no" default=""> <cfargument name="lastName" required="no" default=""> <cfargument name="userID" required="no" default=""> <cfargument name="companyID" required="no" default=""> <cfargument name="getAllRecords" required="no" default="false"> <cfset var qryRead = ""> <cfif not getAllRecords and not len(trim(arguments.firstName)) and not len(trim(arguments.lastName)) and not len(trim(arguments.userID)) and not len(trim(arguments.companyID))> <cfthrow message="Nothing to search by in userDAO.loadUser" detail="Please send something to search on or explicitly specify that you want all records with the getAllRecords argument"> </cfif> <cfquery name="qryRead" datasource="#variables.DSN#"> SELECT userID, firstName, lastName, companyID, systemStatus FROM vUser u WITH(NOLOCK) WHERE systemStatus > 0 <cfif NOT arguments.getAllRecords> <cfif len(trim(arguments.firstName))> AND u.firstName = <cfqueryparam value="#arguments.firstName#"> </cfif> <cfif len(trim(arguments.lastName))> AND u.lastName= <cfqueryparam value="#arguments.lastName#"> </cfif> <cfif len(trim(arguments.userID))> AND u.userID= <cfqueryparam value="#arguments.userID#"> </cfif> <cfif len(trim(arguments.companyID))> AND u.companyID= <cfqueryparam value="#arguments.companyID#"> </cfif> </cfif> </cfquery> <cfreturn qryRead> </cffunction> [/code]The benefit of this approach is that you can be a little more careless with your parameters. You can liberally default them all to empty strings and use named parameter syntax like so:
[code]<cfparam name="form.firstName" default=""> <cfparam name="form.lastName" default=""> <cfparam name="form.userID" default=""> <cfparam name="form.companyID" default=""> <cfset myUser = variables.UserService.loadUser(firstName=form.firstName, lastName=form.lastName, userID=form.userID, companyID=form.companyID)>[/code]The main drawbacks of this approach are that I can't search for an explicit value of an empty string, or NULL. Here is another way of doing it:
[code]<cffunction name="loadUser"> <cfargument name="firstName" required="no"> <cfargument name="lastName" required="no"> <cfargument name="userID" required="no"> <cfargument name="companyID" required="no"> <cfargument name="getAllRecords" required="no" default="false"> <cfset var qryRead = ""> <cfif not getAllRecords and not structkeyexists(arguments,"firstName") and not structkeyexists(arguments,"lastName") and not structkeyexists(arguments,"userID") and not structkeyexists(arguments,"companyID") <cfthrow message="Nothing to search by in userDAO.loadUser" detail="Please send something to search on or explicitly specify that you want all records with the getAllRecords argument"> </cfif> <cfquery name="qryRead" datasource="#variables.DSN#"> SELECT userID, firstName, lastName, companyID, systemStatus FROM vUser u WITH(NOLOCK) WHERE systemStatus > 0 <cfif NOT arguments.getAllRecords> <cfif structkeyexists(arguments,"firstName")> AND u.firstName = <CFQUERYPARAM Value="#arguments.firstName#"> </cfif> <cfif structkeyexists(arguments,"lastName")> AND u.lastName = <CFQUERYPARAM Value="#arguments.lastName#"> </cfif> <cfif structkeyexists(arguments,"userID")> AND u.userID = <CFQUERYPARAM Value="#arguments.userID#"> </cfif> <cfif structkeyexists(arguments,"companyID")> AND u.companyID = <CFQUERYPARAM Value="#arguments.companyID#"> </cfif> </cfif> </cfquery> <cfreturn qryRead> </cffunction> [/code]The benefit of this approach is I like being a bit more explicit about my arguments. I only pass in what I need, and everything I pass in is significant. This allows an empty string to be searched for as a meaningful value. A drawback of this is that I have to be a bit more careful of my input parameters. A value accidentally defaulted to an empty string up the chain will be sent through as a meaningful value. It also necessitates the use of argumentcollection everywhere since I don't know what parameters might be coming through, but I need to preserve only those. It also does not have the ability to match a NULL value specifically.
[code]local.params = {}; if(structKeyExists(form,"firstName") { local.params.firstName = form.firstName; } if(structKeyExists(form,"lastName") { local.params.lastName = form.lastName; } if(structKeyExists(form,"userID") { local.params.userID = form.userID; } if(structKeyExists(form,"companyID") { local.params.companyID = form.companyID; } myUser = variables.UserService.loadUser(argumentcollection=local.params);[/code]As for searching for NULL values, I think I rather detest this method, but I believe it could be achieved in the second example by specifying a keyword that you thought would never actually be used to represent NULL and then checking each parameter like so (Remember, in SQL Server "=" will ALWAYS return false for NULL comparisons-- the "IS" keyword must be used):
[code]<cfif structkeyexists(arguments,"companyID") AND arguments.companyID eq "NULL"> WHERE u. companyID IS NULL <cfelseif structkeyexists(arguments,"companyID")> WHERE u. companyID = <CFQUERYPARAM Value="#arguments.companyID#"> </cfif> [/code]So, there you have it. Sorry to lump so many questions together and then dump a bunch of code on you. Let me know how you handle optional parameters. Do you leave them blank and check for length, or do you simply leave them undefined and check for existence?
Steve Bryant
For myself, I tend to leave them undefined and treat an empty string as a NULL when interacting with the database. This allows me to be more explicit about which values I want to ignore and where I want to set (or filter by) a NULL.
This also works well because a NULL is translated to an empty string in ColdFusion, so I can just pass it directly back in that way without any extra translation. I have yet to find a case where I want to store an actual empty string in the database anyway.
Brad Wood
@Steve: Thanks for the input. You have a good point about treating empty strings as NULLs since ColdFusion treats NULL database values as an empty string.
I'll also agree there aren't many times I actually need to store an empty string in the database. Usually a NULL is desired (in the case of a non-varchar field like a GUID) and since NULLs come back as empty strings anyway, it's kind of 6.5 of one, half a baker's dozen of the other...