Thursday, October 10, 2002

kwfu: ['cfc']
There's been a lot of talk about CFCs recently, and I thought I'd put a few of my ideas and opinions up here.

The first topic that seems to have been getting quite a bit of discussion is the use (or not) of the 'this' scope.

For those of you not familiar with CFCs, the 'this' scope allows you to set a variable from within your component and have it available outside the component.

e.g.

test.cfc
<cfcomponent hint="simple test">
<cfset this.foo = "bar">
</cfcomponent>

invoke.cfm
<cfscript>
obj = createObject('component','test');
</cfscript>

<cfoutput>#obj.foo#</cfoutput>

An alternative to this is to create a method that will allow you to update the value stored in a property. This might be useful in a situation where you have a product component that requires that the price is always an integer value.

e.g.

product.cfc
<cfcomponent hint="simple product component">
<cfset this.name = "">
<cfset this.Description = "">
<cfset this.price = -1>

<cffunction name="setPrice">
<cfargument name="newprice" type="numeric" required="yes">
<cfif int(arguments.newprice) NEQ arguments.newprice>
<cfthrow message="Prices must alwasy be integer values">
<cfelse>
<cfset this.price = arguments.newprice>
</cfif>
</cffunction>
</cfcomponent>

updateprice.cfm
<cfscript>
obj = createObject('component','product');
obj.setPrice(10);
</cfscript>

This works just fine as long as you have control over all the code that is interacting with your component, but it is all too easy for someone to write code like this:

updateprice.cfm
<cfscript>
obj = createObject('component','product');
obj.price = '$10';
</cfscript>

You don't have any way to stop this from happening...

Enter the un-named scope in components.

When you create an un-scoped variable in a component it is created in an un-named scope that is only available to the component and any components that extend it. The fact that components that extend it have access is counter to what the documentation says, and it has been entered as a bug #48483, but that's the way it works at the minute.

e.g.
test.cfc
<cfcomponent hint="simple test component">
<cfset foo = "bar">
</cfcomponent>

invoke.cfm
<cfscript>
obj = createObject('component','test');
</cfscript>
<cfoutput>#obj.foo#</cfoutput>

The code above should throw an error because the variable foo is created in the un-named scope.

If we want to be able to read and write the foo variable in the component we would need to do something like this:

test.cfc
<cfcomponent hint="simple test component">
<!--- create a default value for foo --->
<cfset foo = "">

<!--- This function allows us to get the value of foo from
outside the component --->
<cffunction name="getFoo">
<cfreturn foo>
</cffunction>

<!--- This function allows us to change the value of foo --->
<cffunction name="setFoo">
<cfargument name="newval" type="string" required="yes">
<cfset foo = arguments.newval>
</cffunction>

</cfcomponent>

invoke.cfm
<cfscript>
obj = createObject('component','test');
obj.setFoo('bar');
</cfscript>

<cfoutput>#obj.getFoo()#</cfoutput>

The big advantage of this approach is that you have absolute control of the data from within the component itself. This idea is commonly known as encapsulation and is very important when building complex systems. The reason it is important is that you only have to look inside the component itself to see all the possible ways it's data can be changed.

The downside of using this system is that you have to write a lot more code to create all your get and set functions, and you are likely to have to think carefully about what those functions will be.

e.g.

Suppose you have a much more complex product component that has 50 properties stored in the un-named scope. You probably don't want to have to write 50 get methods to retrieve all the properties out of it, and you probably don't want to put 50 set method calls when you want to update the product, or create a new one. You are most likely going to create methods that will update or retrieve multiple properties at once. You might start with getAll(), getDetails(), and getSummary(). Later you find out that someone needs a specific set of properties for some other reason, and later still a few more methods get added for other groups of properties. Before you know where you are you have a big mess in your component that is very hard to maintain.

You can offset the potential for these problems if you set out from the beginning to plan the application in the same way as many people have been doing for years in the Object Oriented world. There are lots of other places where you can get good information about how to go about planning, so I won't go into it here.

By now, you may well be wondering why you would ever want to put anything into the 'this' scope, because all I've done is tell you that it's a bad thing.

Lets look at another example:

test.cfc
<cfcomponent hint="simple test component">
<!--- create a default value for foo --->
<cfset foo = "">

<!--- This function allows us to get the value of foo from
outside the component --->
<cffunction name="getFoo">
<cfreturn foo>
</cffunction>

<!--- This function allows us to change the value of foo --->
<cffunction name="setFoo">
<cfargument name="foo" type="string" required="yes">
<cfset foo = arguments.foo>
</cffunction>

</cfcomponent>

invoke.cfm
<cfscript>
obj = createObject('component','test');
obj.setFoo('bar');
</cfscript>

<cfoutput>#obj.getFoo()#</cfoutput>

This time, the name of the argument passed to setFoo() is the same as the name of the variable it is setting in the un-named scope. Unfortunately, it doesn't work. No matter how you try to set the variable in setFoo(), getFoo() will always return an empty string. I'm not too sure what's going on to stop the variable assignment from working as expected. It could be that because there is a variable in the 'arguments' scope one is automatically created in the local scope for the function. It could be one of a number of things, but the bottom line is that you won't get an error, and you won't get the variable being set. This is one of the nastiest types of problem. A bug could go months without being discovered, then like a rake in the grass it hits you in the face when you least expect it.

There are a couple of ways to get around this:

The first is to make sure that the variable you are assigning never has the same name as the argument you are using to assign it. This works, but is a bit difficult to maintain and just doesn't seem so elegant as the other option.

The second is to create a pseudo scope as a structure and put all of the properties into this instead. You might call this pseudo scope 'private', 'protected', 'Instance' or even 'variables' since that doesn't seem to exist inside a cfc. Personally I prefer to use 'protected' as that's what I think it maps to best when comparing to other languages.

There are some other interesting things when using components as webservices, but I think I've given you enough to chew on for one blog entry.

Spike