Joel asks:
This is just a quick "best practices" question about CFC's, specifically regarding arguments.Let's say that I am creating a book entry in my database. My form has 3 simple fields--title, author, number of pages. Easy enough.
My question is what a "best practice" for dealing with this scenario would be. Although I am still pretty new to CFC's, I have learned that bundling information as structs to be used in the CFC can be extremely useful, especially when the contents of said struct are dynamic.
But what of known values? Obviously, when invoking the component I could bundle title, author and number of pages into a struct and look for that in the CFC. However, in doing this, it would seem that the various specifications for individual arguments (i.e., 'required', 'type', etc.) would be lost as these could only be applied to the expected struct. Also, it seems that the whole idea of encapsulation is compromised to an extent as the specificity of the arguments being expected is reduced and made more generic (e.g., the struct).
On the other hand, when dealing with ridiculously long forms full of known values, loading them all into a struct would potentially save some coding on the CFC side.
So just in case folks don't quite get what Joel is saying - he is talking about two options to pass data to a CFC. You can either use a set of arguments or have one argument that is a struct. Consider the following two examples:
<cffunction name="test" access="public" returntype="string" output="false">
<cfargument name="name" required="true" type="string" hint="The name of the user.">
<cfargument name="age" required="true" type="numeric" hint="The age of the user.">
<cfreturn "Hello, #arguments.name#, I'm happy you are #arguments.age# years old.">
</cffunction>
<cfset res = test("Ray",34)>
<cfoutput>#res#</cfoutput>
This is how most people set up their methods and here is the alternate way (struct as arguments) that Joel mentioned:
<cffunction name="test2" access="public" returntype="string" output="false">
<cfargument name="data" type="struct" required="true" hint="Struct of arguments.">
<cfreturn "Hello, #arguments.data.name#, I'm happy you are #arguments.data.age# years old.">
</cffunction>
<cfset s = {name="Ray", age=34}>
<cfset res = test2(s)>
<cfoutput>#res#</cfoutput>
So as you can see - Joel is right. In the second example, you no longer have any real clues as to what the UDF is using, outside of one structure. You also lose the validation that helpfully double checked name and age. So in my simple example, I definitely think Joel is right - using "real" arguments does make sense. But as he points out - what about a method that takes many more attributes? It would be simpler, for example, to be able to pass the FORM scope as a structure and not use a lot of...
form.name,form.age,form.foo,form.goo,form.rustillreading
Luckily there are a few things we can to make this easier.
First off - don't forget that you can use CFINVOKE and CFINVOKEARGUMENT syntax. While this results in more typing (potentially), it does make things readable. And you can use these tags without typing until the cows come home. Consider this example:
<cfinvoke component="#application.foo#" method="doIt" returnVariable="result">
<cfloop item="key" collection="#form#">
<cfinvokeargument name="#key#" value="#form[key]#">
</cfloop>
</cfinvoke>
All I've done here is treat the form scope as a structure where each name reflects an argument in my CFC method. You can, if you want, add conditional logic to hide certain form fields, like submit button names, but if your CFC isn't using them, they will simply be ignored. And don't forget the use of attributeCollection:
<cinvoke component="#application.foo#" method="doIt" returnVariable="result" attributeCollection="#form#">
This example is equivalent to the previous form - just more inline and less typing, but again if you did want to skip certain form fields you would not be able to.
So all in all - I agree with Joel here and hopefully the examples above will show ways to mitigate cases where you have to deal with a large number of attributes.
Archived Comments
Ray--
Thanks for response on this. I have been debating with myself over which method to use. However, given the greater flexibility and validation possible with the more explicit argument methodology, I will continue to use that.
Thanks again!
Don't forget you can pass an argumentCollection without using cfinvoke - and it will be treated as individual arguments that can be type checked.
So in your second example you could have declared your arguments in the UDF and passed:
<cfset res = test2(argumentCollection=s)>
Todd has an excellent point. But notice that in both the examples that ray and todd use, you still have to specify the cfargument for each argument you want to pass in that you want to be type checked. And while that can be a drag (and a lot of code) it is, in my opinion still a better option, if nothing else so that your code is more maintainable and readable, but also for the type checking.
I agree with Ryan. Given how easy it is to have code or a CFEclipse snippet generate cfargument tags for you, and given that you really only have to do it one time, there's not much reason to be lazy when it comes to documenting the API for your method.
Dont forget that arguments can be typed arguments, so you could have a single argument be lets say com.blah.myData then if you have multiple cfc's that are using those specific arguments you can create an object of type com.blah.myData and pass that in, you dont lose your documentaion, and you don't have to have the same column names typed everywhere...
True, and that can be a useful option. However, all that really does is move the argument names into the CFC. Which means (if you're still going the argumentCollection route to populate your data container CFC) that you still have to pass in the arguments with the proper names. You're just passing them into the data container CFC instead of the original target CFC by name.
You can also use <cfparam> for the different elements of the struct that you want to validate.
<cfargument name="theStruct" type="struct" />
<cfparam name="arguments.theStruct.numericValue" type="numeric" />
then if you pass in a struct with a key called numericValue that holds a string, it will fail validation as well.
Not ideal but could be used with older code already taking a struct to ensure the validation.
Personally I've gotten in the habit of checking things myself - that way I can handle the issues accordingly. But that's a whole other discussion I think.
We've used both, and if it's just you using your stuff the passing of the struct is a time saver.
And it's definitely convenient when your function can expect a lot of optional parameters.
But I've found that the benefit of cfargument acting as a vehicle to communicate to other programmers what you're expecting, and as an automatic way to enforce things (otherwise you have to do a bunch of validation yourself), outweighs the convenience factor when you have a bunch of developers on the project.
I think David's got the best approach here: when we start dealing in OO systems, it's important to think of the data that moves between method calls not as "parameters" but as a "message" - in this case, it looks like a User object is the message.
It's a few extra keystrokes (wizards abound), but when you properly model, you get a strongly typed API that can handle changes to what a user is (what if a third property gets added?) without regards to implementation. A book called "The OO Thought Process" has a good passage that says something like "once you go beyond a single simple parameter, see if the parameters represent a type of object: if so, model that object and pass it."