Testing For ColdFusion Component Interface Support

Posted April 16, 2009 at 9:21 AM by Ben Nadel

Tags: ColdFusion

This morning, I didn't feel much like coding, so I popped over to the Ruby programming language website and was trying out their 20 minute tutorial. You should check it out, they actually have an online interpretor that lets you run Ruby code directly from the browser! Anyway, in the tutorial, they have this concept of asking objects if they will "Respond" to a given command. In ColdFusion, this would simply be the equivalent of a StructKeyExists() check:

  • <cfif StructKeyExists( objComponent, "MethodName" )> ... </cfif>

But, seeing the idea, it gave me another idea - checking to see if a ColdFusion component supports the interface of another ColdFusion component. From what I have gathered from a few conversations, this is a concept used in Ruby all the time; rather than worrying about "object type", they simply check to see if an object supports a given method.

To play around with this theory in ColdFusion, I created a few small classes that do almost nothing: Person.cfc, Monkey.cfc, Car.cfc. They all extend the base Object.cfc, which I will cover last.

Person.cfc

  • <cfcomponent
  • extends="Object"
  • output="false"
  • hint="I provide person functionality.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an intialized object.">
  •  
  • <!--- Return this object. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Eat"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform an eat action.">
  •  
  • <cfreturn "I just ate!" />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Poop"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a poop action.">
  •  
  • <cfreturn "I just pooped!" />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Talk"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a talk action.">
  •  
  • <cfreturn "I just talked!" />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Walk"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a walk action.">
  •  
  • <cfreturn "I just walked!" />
  • </cffunction>
  •  
  • </cfcomponent>

Monkey.cfc

  • <cfcomponent
  • extends="Object"
  • output="false"
  • hint="I provide monkey functionality.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an intialized object.">
  •  
  • <!--- Return this object. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Eat"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform an eat action.">
  •  
  • <cfreturn "I just ate!" />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Poop"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a poop action.">
  •  
  • <cfreturn "I just pooped!" />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Walk"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a walk action.">
  •  
  • <cfreturn "I just walked!" />
  • </cffunction>
  •  
  • </cfcomponent>

Car.cfc

  • <cfcomponent
  • extends="Object"
  • output="false"
  • hint="I provide car functionality.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an intialized object.">
  •  
  • <!--- Return this object. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Drive"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a drive action.">
  •  
  • <cfreturn "I just drove!" />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Start"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a start action.">
  •  
  • <cfreturn "I just started!" />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Stop"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I perform a stop action.">
  •  
  • <cfreturn "I just stopped!" />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, these objects don't really do anything. They simply define a few public methods. But, if you look again, you'll see that the Monkey component supports a sub-set of the Person functionality. You'll also see that Car doesn't support a sub-set of anybody's functionality.

All three of these components extend a base class, Object.cfc. The key method that I was experimenting with in the base class is SupportsInterface(). This method takes a target CFC class path and checks its interface against the given component. The idea here is that even without ?duck typing?, we can check to see if an object can be "cast" as another object.

  • <cfcomponent
  • output="false"
  • hint="I provide base object functionality.">
  •  
  •  
  • <cffunction
  • name="SupportsInterface"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I determine if this object supports the interface defined by the given component path.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="CFC"
  • type="string"
  • required="true"
  • hint="Path the CFC in question."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!---
  • Get the component meta data of the object with the
  • target interface.
  • --->
  • <cfset LOCAL.MetaData = GetComponentMetaData( ARGUMENTS.CFC ) />
  •  
  • <!---
  • Loop over each method to see if this object instance
  • has a method of the same signature and access.
  • --->
  • <cfloop
  • index="LOCAL.Function"
  • array="#LOCAL.MetaData.Functions#">
  •  
  • <!--- Check to make sure target method is public. --->
  • <cfif (LOCAL.Function.Access EQ "Public")>
  •  
  • <!--- Check for non-equality. --->
  • <cfif NOT (
  • StructKeyExists( THIS, LOCAL.Function.Name ) AND
  • (ArrayLen( LOCAL.Function.Parameters ) EQ ArrayLen( GetMetaData( THIS[ LOCAL.Function.Name ] ).Parameters )) AND
  • (
  • (LOCAL.Function.ReturnType EQ "Any") OR
  • (GetMetaData( THIS[ LOCAL.Function.Name ] ).ReturnType EQ "Any") OR
  • (LOCAL.Function.ReturnType EQ GetMetaData( THIS[ LOCAL.Function.Name ] ).ReturnType)
  • ))>
  •  
  • <!---
  • The two methods do not have the same
  • signature. The interfaces are different.
  • --->
  • <cfreturn false />
  •  
  • </cfif>
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • <!---
  • If we made it this far, then all of the target
  • methods are supported by this object instance.
  • Return true.
  • --->
  • <cfreturn true />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, the SupportsInterface() compares all public methods of both CFCs in question, including their return type and parameters. I am not going in-depth to see if the parameters are the same type, but that could be added if necessary.

To see this concept in action, I created a small test page that compared Person to both the Monkey and Car classes:

  • <!--- Create a person. --->
  • <cfset objPerson = CreateObject( "component", "Person" ).Init() />
  •  
  •  
  • <!--- Check to see if person supports Monkey interface. --->
  • <cfset blnMonkey = objPerson.SupportsInterface( "Monkey" ) />
  •  
  • <!--- Check to see if person supports Car interface. --->
  • <cfset blnCar = objPerson.SupportsInterface( "Car" ) />
  •  
  •  
  • <!--- Output results. --->
  • <cfoutput>
  •  
  • Supports Monkey: <strong>#blnMonkey#</strong><br />
  • Supports Car: <strong>#blnCar#</strong><br />
  •  
  • </cfoutput>

When we run this, we get the following output:

Supports Monkey: true
Supports Car: false

As you can see, the Person class supports the Monkey interface, but not the Car. Based on this, one could make the programmatic assumption that a Person could be used as a Monkey if necessary. I think this concept doesn't make a whole lot of sense for noun-based objects, but I think it could be very cool for behavior-based objects (ie. iteration, collections, etc.).



Reader Comments

Apr 16, 2009 at 9:40 AM // reply »
48 Comments

You know it would be even better if you could have these code segments as a graphic with tab to code view. Reading through the code is tedious just for the sake of getting the concept. I do like the content of these posts and that is just an idea how to get it into minds of more developers faster when the code isn't the actual issue as in this article. It is the existance of the interface.

Of course if that arguments of an itnerface are being tested for also you can have a whole different issue. :)


Apr 16, 2009 at 9:48 AM // reply »
10,640 Comments

@John,

I am not sure what you mean exactly? Like a UML diagram?


Apr 16, 2009 at 10:03 AM // reply »
5 Comments

@Ben - Maybe I'm missing something, but I fail to see where you're implementing an interface. I see a lot of inheritance by extending Object, but no example of implents="interfacename". Am I missing it here?


Apr 16, 2009 at 10:04 AM // reply »
48 Comments

Yes... but don't get so UML standardized that someone who doesn't know UML is clueless. :)

CFC: [Name] (extends : [if applicable])
Methods:
* [method 1]
* [method 2]
+ [required argument] : (data type)
~ [optional argument] : (data type)

Drilling into the rest of the code is nice when concentrating on details but it is much like unit testing. You don't need to know the internals to understand what objects are doing. You just need the internals to understand how to make that function work. :)


Apr 16, 2009 at 10:07 AM // reply »
48 Comments

I forgot to add the return type. That should be there also.

CFC: [Name] (extends : [if applicable])
Methods:
* [method 1] : (returns : dataType)
* [method 2]
+ [required argument] : (data type)
~ [optional argument] : (data type)


Apr 16, 2009 at 10:07 AM // reply »
10,640 Comments

@Andrew,

Exactly! That's the point - there is no real interface being implemented. All the method does is check to see if one object supports the method interface of another. The idea, from what I gather in Ruby, is that if they have the same methods, you simply *trust* that they can be used in the same way.


Apr 16, 2009 at 10:07 AM // reply »
10,640 Comments

@John,

Hmm, I could play around with something like that.


Apr 16, 2009 at 10:09 AM // reply »
5 Comments

@Ben - The idea of an interface is that it is a "contract" as to how the Object will function. Implementation of the interface guarantees that the contract will be adhered to in the implementing class. Assuming that if they have the same methods you can trust them to be used the same way is a dangerous approach. Assumption is the mother of all f**kups, after all.


Apr 16, 2009 at 10:26 AM // reply »
10,640 Comments

@Andrew,

I hear you! I am not saying I necessarily would use this. But, from conversations I've had with Ruby people, apparently, this is a feature they really enjoy.

They even use this in the "20 Minute Tutorial" on the site, when checking to see if an object has the iteration method, each:

elsif @names.respond_to?("each")

Anyway, it just got me thinking is all.


Apr 16, 2009 at 10:33 AM // reply »
12 Comments

Ruby doesn't even have "Interfaces", reminds me of a blog entry I read a while back:

http://danielroop.com/blog/2008/06/28/program-to-an-interface-not-an-interface/


tc
Apr 16, 2009 at 10:52 AM // reply »
5 Comments

Ben,
I am not sure why all this trouble. Why not just use CFINTERFACE?

If one uses CFInterface, then one can simple use the IsInstanceOf() method to test whether the component implements the interface. One can also use the Interface as the "type" specification for the argument passed or return type.

It seems to me that you are going through a lot more work. Is this a support for older CF versions issue?


Apr 16, 2009 at 10:59 AM // reply »
10,640 Comments

@Thomas,

The point is not that an object has an interface contract at compile time. The point is that an object *might* support an interface at run time. An object might even support two or three interfaces at run time (something that cannot work with CFInterface).

For example:

if (
. . . . obj.SupportsInterface( "Iterator" ) AND
. . . . obj.SupportsInterface( "Renderable" )
)

I am not saying that I have the best use-cases for this, but I just thought it was an interesting concept.


Apr 16, 2009 at 12:01 PM // reply »
29 Comments

Neat experiment, Ben! It is sort of a bridge between the Java interface and duck typing of languages like Python and Ruby.

With an interface, you have to explicitly define it in a separate construct/entity with a name. Any class implementing that interface must implement ALL of the methods it defines, not just the ones you need. And most importantly, an interface is treated pretty much like a type at compile time. "This method takes an instance of a class that implements Runnable, fool!"

In dynamic languages, you can pass any object to any method. A method only expects to receive an object that implements the correct set of methods, not an object of a particular class or interface. So a class only needs to implement the methods it actually needs.

This is the essence of duck typing. A method doesn't require an object to be an instance of the Duck class or implement the dozens of methods defined in the Flappable interface. It simply requires that an object have walk() and quack() methods.

The best practice in Python for this sort of duck typing is known as EAFP ("Easier to Ask for Forgiveness than Permission"):

http://en.wikipedia.org/wiki/Python_syntax_and_semantics#Exceptions


Apr 16, 2009 at 12:08 PM // reply »
10,640 Comments

@David,

Ruby seemed nice, from the first tutorial. But, I miss semi-colons. Why oh why do people hate on semi-colons :)


Apr 16, 2009 at 2:50 PM // reply »
38 Comments

The cfcexplorer that comes with CF8 have some code that checks if an object which declares as having some interface X implemented, has all the required methods.

Maybe you will be interested to take a look how Adobe tests obj against cfinterface. :)


Apr 16, 2009 at 4:07 PM // reply »
132 Comments

@John

Ack! If you want to go that route just use IDL.

component extends Super {
public function method( string arg1="default", optional numeric arg2 );
}

There's nothing in there that isn't plain text for another developer.

Notation that uses *, -, +, ~, # and what is horrendously cryptic, especially if you don't follow the UML standard.


Apr 16, 2009 at 4:17 PM // reply »
48 Comments

@Elliot, :) ... I appreciate you having a different opinion. Standards make great guides but poor walls.


Apr 16, 2009 at 6:19 PM // reply »
1 Comments

@Ben: "The point is that an object *might* support an interface at run time. An object might even support two or three interfaces at run time (something that cannot work with CFInterface)."

I don't use interfaces, but from the docs:

A component can implement any number of interfaces. To specify multiple interfaces, use a comma-delimited list with the format interface1,interface2.


tc
Apr 21, 2009 at 10:23 AM // reply »
5 Comments

Ben,
As Matt stated, with CFInterface, one can use multiple interfaces with the implements attribute that was added in CF8 to the cfcomponent tag. I have tested this out and it works fine (e.g., in your case you would state that your component implements="monkey,car" interfaces). One can then use the IsInstanceOf("monkey") method to see if the component implements the desired interface. One can also extend CFInterfaces, like I am doing for a current project, where I implement an IDataObject interface (standard methods of List, GetByPK, Persist, IsPersisted, etc.) and extend it for specific data objects in my model (i.e., IRequestObjects), which implement all the IDataObject interface and add in additional methods, like CheckIn, CheckOut, GetWorkHistory, etc.


Apr 21, 2009 at 10:26 AM // reply »
10,640 Comments

@Matt, @Thomas,

That is cool. I was not aware that a component could implement more than one interface.



Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 12, 2012 at 3:37 AM
Learning ColdFusion 8: CFImage Part III - Watermarks And Transparency
Hi Ben, Just to ask currently it is placed bottom right corner, if i need to replace the same rendered image on the bottom left side or in the bottom center, how that can be calculated. bottom ce ... read »
Feb 11, 2012 at 9:29 PM
Use jQuery's SlideDown() With Fixed-Width Elements To Prevent Jumping
I can't say how glad I am that I found your post. Thank you very much. ... read »
Feb 10, 2012 at 7:21 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
Update! Instead of $(eval(options.insertAfter)).after(data['insertData']); I now use: var ajaxNode = document.createElement('span'); var parent = $(eval(options.insertAfter))[0].parentNode; ... read »
Feb 10, 2012 at 6:18 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API ne ... read »
Feb 10, 2012 at 11:30 AM
Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js
After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/ ... read »
JM
Feb 10, 2012 at 9:10 AM
My Safari Browser SQLite Database Hello World Example
@Amy, Here is a very good tutorial on how to use JOIN: http://www.sqltutorial.org/sqljoin-innerjoin.aspx ... read »
Feb 10, 2012 at 4:42 AM
Building A Twitter-Inspired RESTful API Architecture In ColdFusion
This is great, very useful Ben. I spotted a small typo in the api.cgm listing: <cfthrow type="Unauthroized" /> Cheers Stefan ... read »
Feb 9, 2012 at 10:35 PM
CFDirectory Filtering Uses Pipe Character For Multiple Filters (Thanks Steve Withington)
I was wondering if there would be a filter you could apply so that you got everything but what you included in the filter. As in show me all docs that are not a .pdf. ... read »