Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with:

Using ColdFusion Method Pointers Without Updating ColdFusion Component Methods

By Ben Nadel on
Tags: ColdFusion

The other day, I talked about swapping methods in and out of ColdFusion components at run time. I did this by altering the methods defined in the component object:

  • <!--- Add new method to component. --->
  • <cfset objComp.NewMethod = NewMethodPointer />

... and then was able to call the new method directly, as if it were (and it was) a method of the component itself:

  • <!--- Call new method. --->
  • <cfset objComp.NewMethod() />

This was pretty cool, and it turns out, this is the idea behind ColdFusion Mixins which I had only heard about but never really researched.

What's neat about this is that the method auto wires itself into the appropriate scopes. Meaning, if the method refers to the THIS or the VARIABLES scope, then those references automatically refer to the THIS and VARIABLES scope of the methods new parent scope (ie. the component itself). Nicely done ColdFusion!

To take this one step in another direction, I wanted to see what would happen if you treated methods as behaviors, not as children of the component. To test this, I have created my Stripper object:

  • <cfcomponent
  • displayname="Stripper"
  • output="false"
  • hint="I am a stripper and I am crazy hot!">
  •  
  • <!---
  • Run the pseudo constructor to set up default
  • data structures.
  • --->
  • <cfscript>
  •  
  • // Set up default public properties.
  • VARIABLES.Actions = ArrayNew( 1 );
  •  
  • // Set up clothing properties.
  • VARIABLES.IsWearingShirt = true;
  • VARIABLES.IsWearingPants = true;
  • VARIABLES.IsWearingBra = true;
  • VARIABLES.IsWearingPanties = true;
  •  
  • </cfscript>
  •  
  •  
  • <cffunction name="Init" access="public" returntype="Stripper" output="no"
  • hint="Returns an initialized Stripper instance.">
  •  
  • <!--- Return This reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction name="AddAction" access="public" returntype="void" output="false"
  • hint="Adds an action for the strip routine.">
  •  
  • <!--- Define arguments. --->
  • <cfargument name="Action" type="any" required="true" />
  •  
  • <!--- Add to the actions array. --->
  • <cfset ArrayAppend( VARIABLES.Actions, ARGUMENTS.Action ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction name="Strip" access="public" returntype="string" output="false"
  • hint="I run through the actions you tell me to and then return my status.">
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  • <!--- Loop over actions and perform. --->
  • <cfloop index="LOCAL.ActionIndex" from="1" to="#ArrayLen( VARIABLES.Actions )#" step="1">
  •  
  • <!--- Get method pointer. --->
  • <cfset LOCAL.Action = VARIABLES.Actions[ LOCAL.ActionIndex ] />
  •  
  • <!--- Perform action. --->
  • <cfset LOCAL.Action() />
  •  
  • </cfloop>
  •  
  •  
  • <!--- Get new line. --->
  • <cfset LOCAL.NewLine = "<br />" />
  •  
  • <!--- Build the string buffer for our repsonse. --->
  • <cfset LOCAL.Response = CreateObject( "java", "java.lang.StringBuffer" ).Init( "" ) />
  •  
  • <!--- Add the physical checks. --->
  • <cfset LOCAL.Response.Append(
  • "Wearing Shirt: " & YesNoFormat( VARIABLES.IsWearingShirt ) & LOCAL.NewLine
  • ) />
  •  
  • <cfset LOCAL.Response.Append(
  • "Wearing Pants: " & YesNoFormat( VARIABLES.IsWearingPants ) & LOCAL.NewLine
  • ) />
  •  
  • <cfset LOCAL.Response.Append(
  • "Wearing Bra: " & YesNoFormat( VARIABLES.IsWearingBra ) & LOCAL.NewLine
  • ) />
  •  
  • <cfset LOCAL.Response.Append(
  • "Wearing Panties: " & YesNoFormat( VARIABLES.IsWearingPanties ) & LOCAL.NewLine
  • ) />
  •  
  •  
  • <!--- Return the response. --->
  • <cfreturn LOCAL.Response.ToString() />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, the Stripper keeps track of what articles of clothing she is wearing via several VARIABLES-scoped values. There is also a private array, Actions, which will hold the action the Stripper must perform during her strip tease. We can add actions to this array at any time and then ask the stripper to strip. Once she does this, she returns the status of her clothing. To demonstrate this, let's create a stripper and ask her to strip, having not provided any actions:

  • <!--- Create the stripper. --->
  • <cfset Stripper = CreateObject(
  • "component",
  • "Stripper"
  • ).Init()
  • />
  •  
  • <!--- Tell stripper to strip. --->
  • #Stripper.Strip()#

This gives us the following output:

Wearing Shirt: Yes
Wearing Pants: Yes
Wearing Bra: Yes
Wearing Panties: Yes

Since no actions were assigned, no clothing was removed (oh well).

Now, let's create a few actions that we can send her way.

  • <cffunction
  • name="RemoveShirt"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I remove my shirt... that's kind of hot.">
  •  
  • <!--- Remove shirt. --->
  • <cfset VARIABLES.IsWearingShirt = false />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="RemovePants"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I remove my pants... now we're getting somewhere.">
  •  
  • <!--- Remove pants. --->
  • <cfset VARIABLES.IsWearingPants = false />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="RemoveBra"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I remove my bra... is it cold in here???">
  •  
  • <!--- Remove bra. --->
  • <cfset VARIABLES.IsWearingBra = false />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>

As you can see, all of these functions are referencing the VARIABLES scope. Called on their own, they will not work, as free-floating functions do NOT have VARIABLES scopes. We could set these as built-in methods of the Stripper component, but that is what we have already tested. Let's try adding some of these to the Stripper's Action array and then asking her to strip again:

  • <!--- Add stipper actions. --->
  • <cfset Stripper.AddAction( RemoveShirt ) />
  • <cfset Stripper.AddAction( RemovePants ) />
  • <cfset Stripper.AddAction( RemoveBra ) />
  •  
  • <!--- Tell stripper to strip. --->
  • #Stripper.Strip()#

This gives us the following output:

Wearing Shirt: No
Wearing Pants: No
Wearing Bra: No
Wearing Panties: Yes

Awesome! As you can see, even though the methods were not "part" of the component, the scopes transferred over exactly how we would want them to. And, the structure of the ColdFusion component was not changed at all. Let's take a close look at how these are called:

  • <!--- Loop over actions and perform. --->
  • <cfloop
  • index="LOCAL.ActionIndex"
  • from="1"
  • to="#ArrayLen( VARIABLES.Actions )#"
  • step="1">
  •  
  • <!--- Get method pointer. --->
  • <cfset LOCAL.Action = VARIABLES.Actions[ LOCAL.ActionIndex ] />
  •  
  • <!--- Perform action. --->
  • <cfset LOCAL.Action() />
  •  
  • </cfloop>

As you can see, the Strip method just loops over the actions array, get the method point, and then executes it. Pretty cool. ColdFusion, so dynamic, so freakin' cool!




Reader Comments

Minor nit-pick: free-standing functions *do* have a VARIABLES scope - it refers to the page in which the function is called. A function's VARIABLES scope is dynamically bound in the context in which it is called which is why it binds to the CFC's VARIABLES scope after being injected into the CFC and called as part of object.method()...

Reply to this Comment

Good point. The "context sensitive" Variables binding is pretty cool. It's like the THIS scope in Javascript.

Reply to this Comment

I know this is an old post, but I thought I'd write up a quick thank you to Ben as stumbling into this post via google saved a lot of f**king about.

As a side note, you can also structAppend( baseComponent, helperComponent) if you have a helper class that you need to attach and you can't/don't want to use extends.

Great when using object factories.

Reply to this Comment

@Gabriel,

Using StructAppend() with components! I have never even thought about that. Very cool idea.

Reply to this Comment

See the following scenario (code deliberately abbreviated);

Code:
<cfcomponent>
<cffunction name="init" returntype="myClass">
<cfscript>
// init params, etc
</cfscript>
</cffunction>
</cfcomponent>

On the calling page:
<cfset myClass=createObject('component','myClass').init() />
<cfset myClass.myMethod() />

Obviously that fails... Imagine that as per the example we have created myMethod elsewhere, we append it to myClass
<cfset myClass.myMethod = myMethod />

Now this works...
<cfset myClass.myMethod() />

However, if for any reason you need to use that method INSIDE the class?
<cfcomponent>
<cffunction name="init" returntype="myClass">
<cfscript>
// init params, etc
</cfscript>
</cffunction>

<cffunction name="myInbuiltFunction" returntype="void">
<cfset myMethod() />
</cffunction>
</cfcomponent>

<cfset myClass.myInbuiltFunction() />

Fails because CF can't find the mixed in function.

Why would you want to do this? I have helper functions that I didn't want to have to write into EVERY object of a certain type, I couldn't use extends due to multiple inheritance limitations, blah, blah, blah...

There seems to be 2 main solutions to this problem, which have been blogged about by Joe Rinehart (http://firemoss.net/post.cfm/ColdFusion-Mixins--For-now-Ill-CFInclude) and Sean Corfield (http://corfield.org/blog/index.cfm/do/blog.entry/entry/Mixins) amongst others... using cfinclude and mixins.

Both have pros and cons and I'm not going to bloat this comment any further by debating each, I just wanted to highlight this particular issue and provide links to the other posts incase anyone else stumbles over this thread in search of answers.

(And I'm in the mixin camp, if anyone cares to know.)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.