Friday, 30 May 2014

CFMX7 to CF11: how CFML has progressed

G'day:
In researching my previous article ("Hanging on to outdated knowledge: don't"), I had the displeasure of needing to write some code that would run on ColdFusionMX 7. It caused a lot of swearing, and a lot of "oh FFS, can you not even do that?", but it was cathartic in a way. CFML's really come a long way in between these two versions. And I don't mean pointless shite like <cfpod> and (yeah, I'm gonna...) <cfclient>, but just the language itself.

Here's a test file I knocked together, writing for ColdFusion 9's flavour of CFML. I know I said CF11 in the heading, but I was partially hamstrung by a requirement to have the code run on CF9. CF9 to CF10 is another thing (and then CF10 to 11 too; before one starts thinking about where Railo is taking the language).

// Service.cfc
component {

    public Service function init(required FirstDependency firstDependency, required SecondDependency secondDependency){
        variables.firstDependency    = arguments.firstDependency;
        variables.secondDependency    = arguments.secondDependency;

        return this;
    }

    public void function entryPoint(required Group1 group1, required Group2 group2, Group3 group3, Group4 group4){
        internalMethodRequiringAllOFGroup1(group1=arguments.group1);

        var methodArgs = {
            group1 = group1
        };
        if (structKeyExists(arguments, "group3")){
            methodArgs.group3 = arguments.group3;
        }else{
            methodArgs.group3 = new Group3();
        }
        internalMethodRequiringAllOFGroup1AndGroup3(argumentCollection=methodArgs);

        variables.firstDependency.methodRequiringGroups1And2(group1=arguments.group1, group2=arguments.group2);

        methodArgs.group2 = arguments.group2;
        if (structKeyExists(arguments, "group4")){
            methodArgs.group4 = arguments.group4;
        }else{
            methodArgs.group4 = new Group4();
        }
        variables.secondDependency.methodRequiringAllArgs(argumentCollection=methodArgs);
    }

    private void function internalMethodRequiringAllOFGroup1(required Group1 group1){
    }

    private void function internalMethodRequiringAllOFGroup1AndGroup3(required Group1 group1, required Group3 group3){
    }

}

And here's a functionally-equivalent retrofitted to clunk along on CFMX7:

<!--- Service.cfc --->
<cfcomponent output="false">

    <cffunction name="init" returntype="Service" access="public" output="false">
        <cfargument name="firstDependency" type="FirstDependency" required="true">
        <cfargument name="secondDependency" type="SecondDependency" required="true">

        <cfscript>
        variables.firstDependency    = arguments.firstDependency;
        variables.secondDependency    = arguments.secondDependency;

        return this;
        </cfscript>
    </cffunction>

    <cffunction name="entryPoint" returntype="void" access="public" output="false">
        <cfargument name="group1" type="Group1" required="true">
        <cfargument name="group2" type="Group2" required="true">
        <cfargument name="group3" type="Group3" required="false">
        <cfargument name="group4" type="Group4" required="false">
        <cfscript>
        var methodArgs = structNew();

        internalMethodRequiringAllOFGroup1(group1=arguments.group1);

        methodArgs.group1 = group1;

        if (structKeyExists(arguments, "group3")){
            methodArgs.group3 = arguments.group3;
        }else{
            methodArgs.group3 = createObject("component", "Group3").init();
        }
        internalMethodRequiringAllOFGroup1AndGroup3(argumentCollection=methodArgs);

        variables.firstDependency.methodRequiringGroups1And2(group1=arguments.group1, group2=arguments.group2);

        methodArgs.group2 = arguments.group2;
        if (structKeyExists(arguments, "group4")){
            methodArgs.group4 = arguments.group4;
        }else{
            methodArgs.group4 = createObject("component", Group4).init();
        }
        variables.secondDependency.methodRequiringAllArgs(argumentCollection=methodArgs);
        </cfscript>
    </cffunction>

    <cffunction name="internalMethodRequiringAllOFGroup1" returntype="void" access="private" output="false">
        <cfargument name="group1" type="Group1" required="true">
    </cffunction>

    <cffunction name="internalMethodRequiringAllOFGroup1AndGroup3" returntype="void" access="private" output="false">
        <cfargument name="group1" type="Group1" required="true">
        <cfargument name="group3" type="Group3" required="true">
    </cffunction>

</cfcomponent>

I know opinions vary, but the former looks like proper, clean code that one can be reasonably happy with. The latter one looks like some sort of shitty PlayDoh solution that gives CFML its bad name: HTML pretending to be a scripting language. Bleah.

Note that I as still using CFScript constructs where I can, but CFMX7 was very limited still in this area.

To those people who use tags for everything, how can you look at a script-written CFC next to a tag-written one (even a partial one like that), and not feel a bit bad about your coding style? It's just awful.

Here's the first file again, with some annotations:

// Service.cfc
component {

    public Service function init(required FirstDependency firstDependency, required SecondDependency secondDependency){
        variables.firstDependency    = arguments.firstDependency;
        variables.secondDependency    = arguments.secondDependency;

        return this;
    }

    public void function entryPoint(required Group1 group1, required Group2 group2, Group3 group3, Group4 group4){
        internalMethodRequiringAllOFGroup1(group1=arguments.group1);

        var methodArgs = {
            group1 = group1
        };
        // etc
    }
    // etc
}

And my Application.cfc:

// Application.cfc
component {

    variables.baseSubdir = listLast(getDirectoryFromPath(getBaseTemplatePath()), "\/");

    this.name = "#variables.baseSubdir#23";
    this.applicationTimeout = createTimespan(0,0,0,30);


    function onApplicationStart(){
        application.firstDependency = new "#baseSubdir#.FirstDependency"();
        application.secondDependency = new "#baseSubdir#.SecondDependency"();

        application.service = new "#baseSubdir#.Service"(
            firstDependency = application.firstDependency,
            secondDependency = application.secondDependency
        );

        application.runtime = createObject("java","java.lang.Runtime").getRuntime();
        application.counter = createObject("java", "java.util.concurrent.atomic.AtomicInteger").init();
    }


    function onRequestStart(){
        param URL.slowRequestThreshold=10;
    }

    function onRequest(){
        var startTime    = getTickCount();
        var counter        = application.counter.incrementAndGet();
        var slowWarning    =  "";
        include arguments[1];
        var executionTime = getTickCount()-startTime;
        writeOutput("Execution time: #executionTime#ms");

        if (executionTime > URL.slowRequestThreshold){
            slowWarning = "SLOW REQUEST (above #URL.slowRequestThreshold#ms)";
        }
        writeLog(file="#this.name#", text="#executionTime#;#getFreeAllocatedMemory()#;#counter#;#slowWarning#");
    }

    function getFreeAllocatedMemory(){
        return application.runtime.freeMemory() / 1024^2;
    }

}

Things that CFMX7 doesn't do:

  • Allow script-only components; CFC's have to be coded with tags. WTF?
  • Allow access or type modifiers in a function (or argument) declarations;
  • or requiredness of arguments;
  • struct (or array) literal notation. That said, literal notation is buggy as f*** in CF9, and people are still finding bugs in it in CF11.
  • Creating objects via the new keyword.
  • Not ColdFusion's fault, but it only runs on Java 1.4, and the AtomicInteger class only came in in Java 1.5.
  • No param in script;
  • or include
  • var statements need to be all grouped together, up at the top of the function.
  • script didn't even have proper comparison operators. I can understand not supporting them in tags, but not in script?!
  • No way of logging to file in script.
I won't bother including the code here, but I was also using accessors=true on one of my CFCs to implement synthesised accessors, and obviously that's not part of CFMX7's CFML.

In some of my testing I was using function expressions, which is a staple of my day-to-day coding now, and these are not available in CFMX7 or CF9.

This is not an indictment of CFMX7(*), it's just an observation that the language really has got a lot nicer since then, and it does continue to get more complete and dev-friendly with each new version. Code written in CFMX7 might be a bit ropey, but that was a sign of the times. CFML has come a long way since then. I wonder if most CFML nae-sayers still think all CFML code looks like that tag-based example I showed at the top of the article? Probably. I wish we could disabuse them of the idea that that is where CFML is still at.

--
Adam

(*) other than the fact it was a monumental cock-up on the part of Allaire, Macromedia and Adobe to initially put all their eggs in the tag basket, at the expense of script. I guess I am applying hindsight there a bit. But I speak as someone trying to put all my logic in script since shortly after I started with CFML, back in 2001...