Skip to main content
Ben Nadel at RIA Unleashed (Nov. 2010) with: Bob Silverberg and Carol Loffelmann and Vicky Ryder and Simon Free
Ben Nadel at RIA Unleashed (Nov. 2010) with: Bob Silverberg ( @elegant_chaos ) Carol Loffelmann ( @Mommy_md ) Vicky Ryder ( @fuzie ) Simon Free ( @simonfree )

Testing For ColdFusion Component Interface Support

By on
Tags:

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.).

Want to use code from this post? Check out the license.

Reader Comments

50 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. :)

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?

50 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. :)

50 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)

15,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.

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.

15,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.

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?

15,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.

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

55 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. :)

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.

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.

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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel