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 Scotch On The Rocks (SOTR) 2011 (Edinburgh) with:

Using A Proxy To Give Any Component Method-Chaining Abilities In ColdFusion

By Ben Nadel on
Tags: ColdFusion

When I was looking into using the CFQuery tag in CFScript using ColdFusion 9's new CFScript enhancement, it struck me that these ColdFusion service objects might be a good place to use method chaining. When working with these objects, we are building up values within a given CFC in order to call some sort of execution method on it. It seems that being able to chain the methods calls leading up that final execution would create a nice interface (ala jQuery).

At first, I started to think about extending the Query.cfc class, but then it occurred to me - using ColdFusion 8's OnMissingMethod() functionality (and some CF9 goodness), I can easily create a wrapper for any target CFC that will make the method calls on that target CFC chainable. Essentially, all the wrapper would have to do is proxy the method calls and return itself if the target method didn't return a value. To do this, I came up with the Chainable.cfc:

Chainable.cfc

  • <cfcomponent
  • output="false"
  • hint="I add chainability to my target component.">
  •  
  • <!--- Set properties. --->
  • <cfproperty
  • name="target"
  • type="any"
  • hint="I am the target component that is being made chainable."
  • />
  •  
  • <cfproperty
  • name="targetMethods"
  • type="string"
  • hint="I am the list of target methods that will be made chainable EVEN IF they return a result."
  • />
  •  
  •  
  • <cffunction
  • name="init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an initialized object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="target"
  • type="any"
  • required="true"
  • hint="I am the target component that is being made chainable."
  • />
  •  
  • <cfargument
  • name="targetMethods"
  • type="string"
  • required="false"
  • default=""
  • hint="I am the list of target methods that will be made chainable EVEN IF they return a result."
  • />
  •  
  • <!--- Store the arguments. --->
  • <cfset this.setTarget( arguments.target ) />
  • <cfset this.setTargetMethods( arguments.targetMethods ) />
  •  
  • <!--- Return this object instance. --->
  • <cfreturn this />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onMissingMethod"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I invoke the given method on the target object and chain the results (if possible).">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="missingMethodName"
  • type="string"
  • required="true"
  • hint="I am the name of the method that was invoked."
  • />
  •  
  • <cfargument
  • name="missingMethodArguments"
  • type="struct"
  • required="true"
  • hint="I am the argument collection used to invoke the given method."
  • />
  •  
  • <!--- Invoke the given method on the target object. --->
  • <cfinvoke
  • component="#this.getTarget()#"
  • method="#arguments.missingMethodName#"
  • argumentcollection="#arguments.missingMethodArguments#"
  • returnvariable="local.result"
  • />
  •  
  • <!---
  • Check to see if we have a result. We want to chain
  • the given method if the result is NULL or if the
  • given method was in the list of target methods.
  • --->
  • <cfif (
  • isNull( local.result ) ||
  • listFindNoCase(
  • this.getTargetMethods(),
  • arguments.missingMethodName
  • )
  • )>
  •  
  • <!---
  • The results we got back from the target
  • invocation were either NULL or blacklisted.
  • Return THIS object for method chaining.
  • --->
  • <cfreturn this />
  •  
  • <cfelse>
  •  
  • <!---
  • The result we got back from the target invocation
  • was a valid result. Return it.
  • --->
  • <cfreturn local.result />
  •  
  • </cfif>
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, the Chainable.cfc class doesn't really define any methods at all; it simply proxies calls to the target using OnMissingMethod(). Then, if the target method doesn't return a value, Chainable.cfc returns itself. As I was writing this though, it occurred to me that a lot of times a method will return a value that is not critical. For example, ArrayDelete() returns a boolean as to whether or not an array item was actually deleted. While this is good information to know, I built in the ability to make these sorts of methods chainable as well. The TargetMethods property of Chainable.cfc is a list of methods that will be chained even if they return a value.

With Chainable.cfc defined, I can now wrap my Query.cfc instances making their methods chainable:

  • <cfscript>
  •  
  • // Create a new chainable query object. To do this,
  • // we have to create an instance of our Chainable
  • // component and passour new query instance to it.
  • getGirl = new Chainable( new Query() );
  •  
  • // Set the properties end execute. Notice that these
  • // methods calls are all being chained in one expression.
  • girls = getGirl
  • .setSQL("
  • SELECT
  • id,
  • name,
  • hair
  • FROM
  • girl
  • WHERE
  • name = :name
  • ")
  • .addParam(
  • name = "name",
  • value = "Tricia",
  • cfsqltype = "cf_sql_varchar"
  • )
  • .execute()
  • .getResult()
  • ;
  •  
  • // Output the resultant girl query.
  • writeOutput( "Girl: " & girls.name );
  •  
  • </cfscript>

As you can see, once the Query.cfc instance is wrapped in our Chainable.cfc instance, all native Query.cfc methods are now proxied through Chainable.cfc, making it completely chainable. When we run the above code, we get this output:

Girl: Tricia

Not everyone likes method chaining; and, in some cases, it can certainly make code difficult to read. But, I think that at times, it can really clean things up. In cases like this, for example, where several different method calls all relate to each other and are building up to a single execution, I think it is a really nice option.




Reader Comments

@Jared,

There will be some overhead since there is a proxy object and additional logic. However, from everything that I've been told, they built OnMissingMethod() functionality to be extremely fast because it was designed specifically to add logic around method calls.

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.