Friday, 20 July 2012

Which is better: having your methods inline in a CFC, or included from a separate file?

A question came up on StackOverflow about whether it's "better" to compose a CFC vis a single CFC file, out to include the methods via <cfinclude>. Thus is an interesting topic (after a fashion), so I decided to look into it some more, beyond the answer to the specific question on StackOverflow.

Just to set a baseline, what Henry was asking is whether there are performance considerations when contrasting these two approaches to composing a component:

<!--- MethodsInline.cfc --->
<cfcomponent>
    
    <cffunction name="publicMethod" access="public" returntype="struct" hint="This is my public method">
        <cfreturn arguments>
    </cffunction>
    
    <cffunction name="privateMethod" access="private" returntype="struct" hint="This is my private method">
        <cfreturn arguments>
    </cffunction>
    
    <cffunction name="packageMethod" access="package" returntype="struct" hint="This is my package method">
        <cfreturn arguments>
    </cffunction>
    
    <cffunction name="remoteMethod" access="remote" returntype="string" hint="This is my remote method">
        <cfargument name="who" type="string" required="true">
        <cfreturn "hello, #arguments.who#">
    </cffunction>
    
</cfcomponent>



<!--- MethodsViaInclude.cfc --->
<cfcomponent>
    
    <cfinclude template="./methodsViaInclude.cfm">

    <cffunction name="inlineRemoteMethod" access="remote" returntype="string" hint="This is my remote method">
        <cfargument name="who" type="string" required="true">
        <cfreturn "hello, #arguments.who#">
    </cffunction>
    
</cfcomponent>

<!--- methodsViaInclude.cfm --->

<cffunction name="publicMethod" access="public" returntype="struct" hint="This is my public method">
    <cfreturn arguments>
</cffunction>

<cffunction name="privateMethod" access="private" returntype="struct" hint="This is my private method">
    <cfreturn arguments>
</cffunction>

<cffunction name="packageMethod" access="package" returntype="struct" hint="This is my package method">
    <cfreturn arguments>
</cffunction>

<cffunction name="includedRemoteMethod" access="remote" returntype="string" hint="This is my remote method">
    <cfargument name="who" type="string" required="true">
    <cfreturn "hello, #arguments.who#">
</cffunction>


The first file listing has the methods inline; the second two file listing compose a CFC via shell CFC and then including a file with the actual methods.

The first thing to consider is the answer to the actual question: performance.

Well one thing that warrants mention here is that ColdFusion didn't actually execute the CFM files, all that happens to them is that they are compiled, and then the resultant class files are loaded and executed. The compile process happens one when the CFML file is first hit, and then each time the file changes, or if for some reason the compiled class is removed from memory, and if the class file had not been compiled to disk. By default, the compiled classes are saved to [ColdFusion dir]\WEB-INF/cfclasses (on my machine that's C:\Apps\JRunservers\cf9.local\cfusion.ear\cfusion.war\WEB-INF\cfclasses). Given how this works, the composition of the CFML files do not contribute a huge amount to this sort of performance: a file will be read far far more often than it is changed (necessitating a recompile), and if your server is configured properly, most of your classes will stay in memory once they're needed anyhow.

For completeness, one should consider the compiled class files too, in case there's any massive overhead presented by one or the other approach.  I'm not going to reproduce the decompiled code here (because it's not the clearest code in the world, and is not very helpful to look at), but it's mostly the same.  As far as the files themselves go, we end up with this:

cfMethodsInline2ecfc876167990$funcPACKAGEMETHOD.class
cfMethodsInline2ecfc876167990$funcPRIVATEMETHOD.class
cfMethodsInline2ecfc876167990$funcPUBLICMETHOD.class
cfMethodsInline2ecfc876167990$funcREMOTEMETHOD.class
cfMethodsInline2ecfc876167990.class 
cfMethodsViaInclude2ecfc1581372422$funcINLINEREMOTEMETHOD.class
cfMethodsViaInclude2ecfc1581372422.class
cfmethodsViaInclude2ecfm1581372444$funcINCLUDEDREMOTEMETHOD.class
cfmethodsViaInclude2ecfm1581372444$funcPACKAGEMETHOD.class
cfmethodsViaInclude2ecfm1581372444$funcPRIVATEMETHOD.class
cfmethodsViaInclude2ecfm1581372444$funcPUBLICMETHOD.class
cfmethodsViaInclude2ecfm1581372444.class 
cfremote2ecfm1868968566.class

Note that there's one file for each CFC, one file for each method, and an extra file for the included file.  And also one for my calling code. So there's an extra file if one choses to include the methods.  I would not consider this a meaningful overhead.  Or any sort of overhead.

I didn't bother actually contriving some sort of performance test here, because I think it's clear there is not going to be any real difference.  Trying to optimise this sort of performance falls under the realms of premature optimisation. Or: "simply not worth considering".

So performance isn't a consideration.  What other considerations are there.  Well: the two approaches are not actually the same, and the include-your-methods approach has significant draw backs.

Look at this code, and its output:

<cfscript>
    objects.o1 = createObject("MethodsInline");
    metadata.o1 = getMetadata(objects.o1);

    objects.o2 = createObject("MethodsViaInclude");
    metadata.o2 = getMetadata(objects.o2);

    writeDump(var=metadata.o1, label="metadata.o1");
    writeOutput("<br/><br/>");
    writeDump(var=metadata.o2, label="metadata.o2");
</cfscript>


metadata.o1 - struct
EXTENDS
metadata.o1 - struct
FULLNAMEWEB-INF.cftags.component
NAMEWEB-INF.cftags.component
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\component.cfc
TYPEcomponent
FULLNAMEblog.includedMethods.MethodsInline
FUNCTIONS
metadata.o1 - array
1
metadata.o1 - struct
ACCESSremote
HINTThis is my remote method
NAMEremoteMethod
PARAMETERS
metadata.o1 - array
1
metadata.o1 - struct
NAMEwho
REQUIREDtrue
TYPEstring
RETURNTYPEstring
2
3
4
NAMEblog.includedMethods.MethodsInline
PATHD:\websites\www.scribble.local\blog\includedMethods\MethodsInline.cfc
TYPEcomponent


metadata.o2 - struct
EXTENDS
metadata.o2 - struct
FULLNAMEWEB-INF.cftags.component
NAMEWEB-INF.cftags.component
PATHC:\Apps\JRunservers\cf9.en01.hostelbookers.local\cfusion.ear\cfusion.war\WEB-INF\cftags\component.cfc
TYPEcomponent
FULLNAMEblog.includedMethods.MethodsViaInclude
FUNCTIONS
metadata.o2 - array
1
metadata.o2 - struct
ACCESSremote
HINTThis is my remote method
NAMEinlineRemoteMethod
PARAMETERS
metadata.o2 - array
1
metadata.o2 - struct
NAMEwho
REQUIREDtrue
TYPEstring
RETURNTYPEstring
NAMEblog.includedMethods.MethodsViaInclude
PATHD:\websites\www.scribble.local\blog\includedMethods\MethodsViaInclude.cfc
TYPEcomponent

(I've collapsed a bit of the first dump to save space, but you get the idea)

The second object is missing the bulk of its metadata: none of the methods from the include are displayed.

If you're writing an API, this has a knock-on effect that the docs CF generates for you are also truncated (because they're based on the metadata):

The documentation for MethodsInline.cfc are complete:

blog.includedMethods.MethodsInline
Component MethodsInline 



hierarchy:WEB-INF.cftags.component
      blog.includedMethods.MethodsInline
path:D:\websites\www.scribble.local\blog\includedMethods\MethodsInline.cfc
serializable:Yes
properties:
methods:packageMethod, privateMethod*, publicMethod, remoteMethod
* - private method 

packageMethod
package struct packageMethod ( ) 

This is my public method

Output: 
privateMethod*
private struct privateMethod ( ) 

This is my private method

Output: 
publicMethod
public struct publicMethod ( ) 

This is my public method

Output: 
remoteMethod
remote struct remoteMethod ( ) 

This is my public method

Output: 

The documentation for MethodsViaInclude.cfc are sadly lacking:

blog.includedMethods.MethodsViaInclude
Component MethodsViaInclude 



hierarchy:WEB-INF.cftags.component
      blog.includedMethods.MethodsViaInclude
path:D:\websites\www.scribble.local\blog\includedMethods\MethodsViaInclude.cfc
serializable:Yes
properties:
methods:inlineRemoteMethod
* - private method 

inlineRemoteMethod
remote string inlineRemoteMethod ( required string who ) 

This is my remote method

Output: 
Parameters:
   who: string, required, who 

This is still a bit "cosmetic-y".  Here's an actual function problem.

Consider this code, making a call to the CFC's remote methods:

<cfscript>
    webservices.oMethodsInlineWs = createObject("webservice", "http://www.scribble.local/blog/includedMethods/MethodsInline.cfc?wsdl");
    
    results.fromInlineWebService = webservices.oMethodsInlineWs.remoteMethod(who="Tim");
    writeDump(var=results.fromInlineWebService, label="results.fromInlineWebService");

    webservices.oMethodsViaIncludeWs = createObject("webservice", "http://www.scribble.local/blog/includedMethods/MethodsViaInclude.cfc?wsdl");
    try {
        results.fromIncludeWebService = webservices.oMethodsViaIncludeWs.includedRemoteMethod(who="Graeme");
        writeDump(var=results.fromIncludeWebService, label="results.fromIncludeWebService");
    } catch (any e){
        results.fromIncludeWebService = "#e.message# #e.detail#";
    }
    results.fromIncludeWebServiceInlineMethod = webservices.oMethodsViaIncludeWs.inlineRemoteMethod(who="Bill");
    
    writeDump(results);
</cfscript>

You can probably guess from my usage of try/catch in that where I'm going with this. Here's the output:

struct
FROMINCLUDEWEBSERVICEWeb service operation includedRemoteMethod with parameters {who={{who, Graeme}}} cannot be found.
FROMINCLUDEWEBSERVICEINLINEMETHODhello, Bill
FROMINLINEWEBSERVICEhello, Tim

CF doesn't expose the "remote" method in methodsViaInclude.cfm as remote. This can also be borne out by looking at the WSDL, which only shows the inline remote method (I'll spare you the output of the WSDL!).

I also tried this test with the private method, but that worked fine.  That's something.  I did not test with package methods, but I would guess they'd be fine too, as I suspect it's the way that CF looks for web services that is at fault here, rather than the access levels intrinsically.

Another consideration is that the include approach kinda breaks CFC inheritance a bit.  Look at this example:

<!--- Parent.cfc --->
<cfcomponent>

    <cffunction name="someMethod">
    </cffunction>
    
</cfcomponent>

<!--- Child.cfc --->
<cfcomponent extends="Parent">

    <cfinclude template="childMethods.cfm">
    
</cfcomponent>

<!--- childMethods.cfm --->

<cffunction name="someMethod">
</cffunction>


<!--- testOverride.cfm --->
<cfset o = createObject("Child")>
<cfdump var="#o#">

Running testOverride.cfm yields this compile error:

The following information is meant for the website developer for debugging purposes.
Error Occurred While Processing Request

Routines cannot be declared more than once.

The routine someMethod has been declared twice in different templates.
ColdFusion cannot determine the line of the template that caused this error.This is often caused by an error in the exception handling subsystem.

This is because of the way CF compiles different file types: CFM files don't have the notion of object orientation, so inheritance and method overriding is irrelevant, so when including a CFM file, it breaks.

Another reason still is in CF Builder, the "Outline" view doesn't list all the methods in the CFC if all the methods are included.  Well: it does list all the methods in the CFC, but that's not all the methods that comprise the component though.

All in all... I reckon it's a shoddy practice, and causes problems.

--
Adam