Guess what time it is, kids!!
It's "Barney still wants CFML closures" time! Yay!
Today's impetus is Edmund, Sean Corfield's event driven programming framework. In order to register event listeners, you have to a CFC instance with a specific method to be invoked on it, and which accepts an Edmund Event as it's sole argument. Which means you have to have these silly little CFCs hanging about that simply get the event and then hand it off to the appropriate business components to actually do stuff. Yes, I understand that's a very OO way to do it: lots of little, purpose-specific types passing messages between them. But it's a bitch with CFML because every type has to be it's own file and you don't get context inheritance.
In Java most of the time your event listeners are anonymous inner classes – instances of classes that are defined inline. That mechanism gets a lot of grief, and while I agree that it's a little more verbose than necessary in simple cases, it's a lot better than the crap that static typing and/or checked exceptions foists on you, and having a full class definition can be useful as things get more complex.
Groovy smoothes that a little by allowing you to define Closures and use them instead. Under the hood, it's just converting them to your standard Java anonymous inner classes, but the syntax is rather nicer. If you know Ruby, think lambdas.
This is what I'd do in Java:
edmund.register("eventCreated", new GenericHandler() {
public handleEvent(Event e) {
beanFactory.getBean("eventservice").postprocessEvent(e.value("eventId"));
}
});
or in Groovy:
edmund.register("eventCreated", {
beanFactory.getBean("eventservice").postprocessEvent(e.value('eventId'))
})
But in CFML, I need to create a separate type (in a separate file):
<cfcomponent>
<cffunction name="init">
<cfargument name="beanFactory" />
<cfset variables.beanFactory = arguments.beanFactory />
</cffunction>
<cffunction name="handleEvent">
<cfargument name="e" />
<cfset beanFactory.getBean("eventservice").postprocessEvent(e.value("eventId")) />
</cffunction>
</cfcomponent>
And then register it like this:
<cfset edmund.register("eventCreated",
createObject("component", "MyEventHandler").init(beanFactory)
) />
What a mess. Not only do I have to create a separate file to have that one single line of code in it (line 10), but I also have to worry about passing context around (in this case, the beanFactory), because the CFC instance doesn't inherit the context it's instantiated in (as closures and anonymous inner types do). And this is a ridiculously trivial example. Here's what I'd like to see in CFML:
edmund.register("eventCreated", function(e) {
beanFactory.getBean("eventservice").postprocessEvent(e.value("eventId"));
});
You can see that I'm following the ECMAScript-like nature of CFML expressions and stealing ECMAScript's function literal syntax (one of them, at least) to create an anonymous function. In order for this to work, it'd have to be a closure (as ECMAScript functions are), not just a context-free function. As an alternative (which is more complicated, but which has certain advantages in certain scenarios), would be to have a CFC literal (anonymous inner CFC) as well:
edmund.register("eventCreated", new component() {
function handleEvent(e) {
beanFactory.getBean("eventservice").postprocessEvent(e.value("eventId"));
}
});
Personally, I'd much prefer the closure if I only got one, but both would be nice. The semantics of anonymous inner types can be a little wonky, and the advantages over simple closures is small, but they can still be really useful. Now that we have a CFSCRIPT-based way of defining components, the language at least has syntactic constructs to express anonymous types, so it's theoretically possible.
So since I can't do all this neat stuff, you might ask what I did do. I used ColdSpring with a purpose-sepecific adapter I wrote, along with a custom extension to Edmund to allow registering listeners from ColdSpring. So my Edmund config looks like this:
<bean id="edmund">
<constructor-arg name="asyncByDefault"><value>false</value></constructor-arg>
<property name="eventListeners">
<map>
<entry key="eventCreated">
<bean class="edmund.framework.ColdSpringListener">
<constructor-arg name="handler"><ref bean="eventservice" /></constructor-arg>
<constructor-arg name="method"><value>postprocessEvent</value></constructor-arg>
</bean>
</entry>
</property>
</bean>
The ColdSpringListener bean simply takes care of delegating the configured method to the supplied bean when it is triggered, passing along the event arguments along. This obviously is still pretty verbose, but the per-listener overhead is four lines of XML (in an existing file), not a 13 line CFC (in a new file). That counts as a win by me. The way I extended Edmund allowed passing a single listener or an array of listeners. The method I added is here:
<cffunction name="setEventListeners" returntype="any" access="public" output="false"
hint="I register multiple new listeners at once. I DO NOT REPLACE listeners,
despite being a setter. I should be named addMultipleEventListeners, but must
be named setEventListeners so I can be used from ColdSpring.">
<cfargument name="listeners" type="struct" required="true"
hint="I am a struct with event names as keys and either a single listener or
array of listeners as values. Note that there is no way to specify the
handler method or whether listeners are asynchronous." />
<cfset var e = "" />
<cfset var i = "" />
<cfloop collection="#listeners#" item="e">
<cfif NOT isArray(listeners[e])>
<cfset i = listeners[e] />
<cfset listeners[e] = arrayNew(1) />
<cfset arrayAppend(listeners[e], i) />
</cfif>
<cfloop from="1" to="#arrayLen(listeners[e])#" index="i">
<cfset register(e, listeners[e][i]) />
</cfloop>
</cfloop>
<cfreturn this />
</cffunction>
So is this good stuff? It's OK. It's solid for a CFML solution. But there is no question that it pales in comparison to the elegance with which you could solve the problem in other languages. Java (same age as CFML) has this, Ruby (a little older than CFML) has this, Python (much older than CFML) has this, and don't even get me started on Lisp (more than double the age of CFML). It's not a concept new to computer science.
Alright. Rant over. Until next time I seriously consider rebuild huge swaths of CFML in Groovy just for closures and start yelling again.