Skip to main content
Ben Nadel at the New York ColdFusion User Group (Apr. 2008) with: Clark Valberg and Nafisa Sabu and Rob Gonda
Ben Nadel at the New York ColdFusion User Group (Apr. 2008) with: Clark Valberg Nafisa Sabu Rob Gonda

Creating Ruby-Inspired Modules In ColdFusion

By
Published in Comments (8)

A couple of months ago, I read Practical Object-Oriented Design in Ruby: An Agile Primer, by Sandi Metz. I haven't reviewed the book yet, but I wanted to explore one of the concepts Metz talked about: Ruby Modules. From what I understood (and this may be somewhat off-base, I'm not a Ruby programmer), a module is a way to "inherit" behavior without using classical inheritance. Essentially, behavior is "included" into a class, rather than "inherited" into a class. In a language that only allows for single-class inheritance, a Module provides a mechanism for including behavior from several different sources. Furthermore, it allows the developer to borrow behaviors without worrying about the "is-a-type-of" inheritance relationship. It seems fairly interesting, so I wanted to see how this kind of behavior could be used in ColdFusion.

In the past, I've looked at "mixins" in ColdFusion. Essentially, a mixin is just the "include" of a code block into another container. ColdFusion's CFInclude tag allows you to include both .cfm and .cfc files. However, with a .cfc file, the CFComponent tag is completely ignored (treating the .cfc file as if it were a .cfm file). I think we can use the CFInclude tag to mimic (in part) the Module concept in Ruby.

To explore this, I wanted to create a publish-and-subscribe module. This module would expose two public methods, on() and off(), for event binding and unbinding, respectively; it would expose one private method, trigger(), such that the container component could announce (ie. publish) events.

To start, I created my PubSub.cfc ColdFusion component - this component will be our "Ruby Module." Since this component is not meant to be instantiated, I am excluding any init() method. Therefore, any instance variables that the module will create need to be defined in the component's pseudo-constructor. The module methods can then be defined the same way any ColdFusion component methods could be defined:

PubSub.cfc - Our Ruby-Inspired "Observable" Module

<cfscript>

component
	output = false
	hint = "I provide simple publish + subscribe module features."
	{


	// Each event type will have any number of subscriptions associated
	// with it. Each subscription will be characterized as a type and a
	// function callback.
	eventTypes = {};


	// ---
	// PUBLIC METHODS.
	// ---


	// I remove the subscription from the given even type.
	public any function off(
		required string eventType,
		required any eventHandler
		) {

		// If the event type doesn't exist, nothing else to do.
		if ( ! structKeyExists( eventTypes, eventType ) ) {

			return( this );

		}

		var filteredSubscriptions = [];

		// Copy over all of the subscriptions that do not match the
		// given event handler.
		for ( var subscription in eventTypes[ eventType ] ) {

			if ( isSameHandler( subscription.handler, eventHandler ) ) {

				continue;

			}

			arrayAppend( filteredSubscriptions, subscription );

		}

		// Persist the filtered subscription collection.
		eventTypes[ eventType ] = filteredSubscriptions;

		// Return the reference to the consumer of the module.
		return( this );

	}


	// I set up the subscription to the given event type.
	public any function on(
		required string eventType,
		required any eventHandler
		) {

		// Make sure that we have a subscription channel for the given
		// event type. Currently, event types are completely arbitrary.
		if ( ! structKeyExists( eventTypes, eventType ) ) {

			eventTypes[ eventType ] = [];

		}

		// Add the subscription.
		arrayAppend(
			eventTypes[ eventType ],
			{
				type = eventType,
				handler = eventHandler,
				uuid = getEventHandlerID( eventHandler )
			}
		);

		// Return the reference to the consumer of the module.
		return( this );

	}


	// ---
	// PRIVATE METHODS.
	// ---


	// I param a module-based UUID for the event handler meta data
	// such that different handlers can be compared for equality. This
	// data is persisted in the function's meta data.
	private string function getEventHandlerID( required any eventHandler ) {

		var uuidKey = "pubsub-id";
		var metaData = getMetaData( eventHandler );

		if ( ! structKeyExists( metaData, uuidKey ) ) {

			metaData[ uuidKey ] = createUUID();

		}

		return( metaData[ uuidKey ] );

	}


	// I determine if the two event handlers are the same.
	private boolean function isSameHandler(
		required any eventHandlerA,
		required any eventHandlerB
		) {

		var uuidA = getEventHandlerID( eventHandlerA );
		var uuidB = getEventHandlerID( eventHandlerB );

		return( uuidA == uuidB );

	}


	// I trigger the given event on the given channel.
	private void function trigger( required string eventType ) {

		// If there are no bindings on this channel, there is nothing
		// more that we need to do.
		if ( ! structKeyExists( eventTypes, eventType ) ) {

			return;

		}

		// Invoke each handler with same argument collection. This
		// way, the event can be triggered with multiple arguments.
		for ( var subscription in eventTypes[ eventType ] ) {

			subscription.handler( argumentCollection = arguments );

		}

	}


}

</cfscript>

The details the of the code are not so important. Just note that the component (ie. our Module) has defined an instance variable - eventTypes - and several public and private methods.

NOTE: I could have left out the CFComponent tag in our PubSub.cfc module; however, I wanted to keep it there as a way to philosophically define the module as one cohesive and isolated set of features.

Now, let's create a normal ColdFusion component that includes this module file in order to leverage the publish and subscribe behavior. In this case, we'll create a Friend.cfc ColdFusion component that exposes a setName() method; every time the setName() method is called, a "nameChanged" event is triggered.

Friend.cfc - Our Consumer Of PubSub.cfc Module

<cfscript>

component
	output = false
	accessors = true
	hint = "I model a Friend with evented setters."
	{


	// Define the properties for accessor generation.
	property name="name" type="string";


	// Include the PubSub behaviors, on(), off(), trigger().
	include "PubSub.cfc";


	// I initialize the component with the given default values.
	public any function init( required string initialName ) {

		name = initialName;

		return( this );

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// Set the new name - triggers "nameChanged" event.
	public any function setName( required string newName ) {

		var oldName = name;

		name = newName;

		// Announce the name-change event to all callback handlers
		// that have asked to subscribe.
		trigger( "nameChanged", newName, oldName );

		return( this );

	}


}

</cfscript>

Notice that once the PubSub.cfc module is included into the Friend.cfc component, the component can make use of the "inherited" trigger() method as if it were a native method of the component.

Ok, now it's time to test this baby beast out. In the following code, I've created two different event handles for the "nameChanged" event. Both our bound using the "inherited" on() method. Then, one is unbound using the "inherited" off() method. I do this to make sure that both the on() and off() methods are working properly.

<cfscript>


	// Define one name-change handler.
	function nameChangeHandler( eventType, newName, oldName ) {

		writeOutput( "[1] Name Changed: " & oldName & " to " & newName );
		writeOutput( "<br />" );

	}


	// Define another name-change handler so that we can test to see
	// if the off() method unbinds the correct handler.
	function anotherNameChangeHandler( eventType, newName, oldName ) {

		writeOutput( "[2] Love your new name: " & newName );
		writeOutput( "<br />" );

	}


	// NOTE: In ColdFusion 9, the above event handlers will NOT have
	// access to this page's variables' scope. ColdFusion 9 does not
	// create closures; however, in ColdFusion 10, you CAN create a
	// closure that does keep the proper variables reference.


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// Create our friend instance - this instance includes the PubSub
	// module and should have publicly exposed methods for on() and
	// off() binding and unbind respectively.
	friend = new Friend( "Sarah" );

	writeOutput( "Initial Name: " & friend.getName() & "<br />" );


	// Bind our two event handlers.
	friend.on( "nameChanged", nameChangeHandler );
	friend.on( "nameChanged", anotherNameChangeHandler );

	// Change the name to see that BOTH event handlers are triggered.
	friend.setName( "Tricia" );

	// Unbind one of the event handlers.
	friend.off( "nameChanged", nameChangeHandler );

	// Change the name again to make sure that only ONE of the event
	// handlers is triggered, ensuring that the other event handler
	// was properly unbound via off().
	friend.setName( "Joanna" );


</cfscript>

When we run the above code, we get the following page output:

Initial Name: Sarah
[1] Name Changed: Sarah to Tricia
[2] Love your new name: Tricia
[2] Love your new name: Joanna

As you can see, the event handlers were triggered when the setName() method was called. Furthermore, we know that the off() method worked properly since only one of the event handlers was invoked at the second calling of the setName() method.

NOTE: In ColdFusion 9 (and earlier), our event handlers are not bound to the Variables scope of our test code. A the time of invocation (ie. triggering), they are bound to the context of the Friend component instance. As of ColdFusion 10, however, closures can be used to maintain the intended Variables scope binding.

I believe that Ruby modules are actually a bit more robust than this. I believe that a Ruby module can have both "module methods" and "instance methods." This could probably be mimicked using a private "namespace" variable; but even then, defining module methods would be a bit messy. If you really wanted to copy the Ruby Module behavior, you'd probably have to use something more like a ColdFusion custom tag mixin, which has its own sandboxed Variables scope.

This kind of Ruby module architecture is very interesting. It allows you to create inheritable behavior without having to think about an actual class inheritance chain. Since I'm not a good Object-Oriented developer, I won't try to talk about the pros and cons of such an approach; but, with behavior like Publish and Subscribe, I can easily see the value of mixing-in behavior without having to worry as to whether or not, "Friend is a type of Observable," as you would with classical inheritance.

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

Reader Comments

8 Comments

I have just started to pick up Ruby in the last few months. Being able to include functionality so easily with a Gem or a module makes extending functionality so much easier. I like the idea of applying that rational to CF.

5 Comments

Hi Ben,

Actually i am working on a framework which is a totally event driven approach with using similar concept but little bit with around advice to an event. example.

user.around("save", "Transaction')
.on("save", "email.userConfirmation")
.on("change:['password']."notify.admin")

and do have more functionality planned. i happend to support an legacy application and running into lot off issues making changes luckily application is running on CF10 so closure is to rescue. i don't know ruby i do get lot of concepts from javascript and implementing on CF just for fun luckily got approval and use in a new project. here is an another JS library i was copying http://hood.ie/

good to see i am not the only crazy about implementing js concept in CF

love you blog posts

15,848 Comments

@Billy,

This code is running in CF9 - the comment about CF10 was that it has "closures" which make the function-passing a bit different.

15,848 Comments

@Brad,

I've played with Ruby for about 3 days (in total). So, I have very little understanding about how it all works. I'd be curious to hear about a few (or even just one) Gems that you have worked with; and, how they work well as modules. I'm just interested in how they get used.

15,848 Comments

@Manithan,

That looks really interesting. But, what is the second argument in the methods you are calling? Is that supposed to be a String? Or would that be a reference to an object/function? Like, I understand that you want to implement Transaction around the save method; but, who "does" the Transaction. Is this like AOP - aspect oriented programming? I think in AOP, they call those wrapper-features, "advice"? Anyway, looks really interesting!

5 Comments

@Ben,

second arguments could be a call back function or string (in my framework it is an event like user.save would be user.cfc and save function just like coldbox or fw1 ) or an array of events. around is AOP call where i add transaction. this case "Transaction" in an interceptor which would have an aroundAdvice function which event call would go through from event dispatcher. i was basically trying to make a simple AOP without create proxy object or CFC like coldspring and wirebox does using closures where i think perfect used case for me.

i was planning to implement before and after advice but aroundAdvice perfectly fits right where i can control the event. i have lot of things going on like javascript events in it.

not sure how it fit for new applications since there are lot of frameworks out there but for me it fits for legacy application where i don't have to change much code. just configure the events and dispatch them from right place.

15,848 Comments

@Manithan,

Ah, OK, I completely see what you're saying. The strings maps to a component method. Seems pretty cool! I don't know much about how frameworks currently implement this kind of stuff - I'm still getting my feet wet with object technologies.

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