Tuesday, 17 February 2015

CFML: Interface fulfilment via mixin

G'day:
So a day after I say I'm gonna leave this blog be for a while, I'm back. It's like how I said I was ditching CFML and going to PHP on this blog. Basically I just lie, don't I?

Well I'm sick of writing this bloody book for the day, and I've got a reader question to answer which I decided to follow up on. But first I did some prep work and found something moderately intriguing. Well as intriguing as CFML's interface implementation gets, anyhow.

We've been using mixins a bit at work (this is all PHP stuff) recently to work around some "challenges" our (homebrewed) framework throws at us, and this has got me thinking about the same thing on CFML. I've requested a formal approach to mixins for CFML before, but I'm damned if I can find the ticket(s) for any of ColdFusion, Railo or Lucee now.

But, really, just including a file with some functions in it in a CFC ought to do the trick. So here's some test code. First, the interface I'm using for all my tests.

// I.cfc

interface {
    function f(required numeric x);
}

Next a control which is just normal interface usage:

// UsingInline.cfc

component implements="I" {
    function f(required numeric x){
        return x;
    }
}

// usingInline.cfm

o = new UsingInline();
writeOutput(o.f(1));

This just outputs 1. Boring, but is just a baseline, like I said. No surprises.

Right, but now let's try a mixin:

// UsingMixin.cfc

component implements="I" {
    include "functions.cfm";
}

// usingMixin.cfm

o = new UsingMixin();
writeOutput(o.f(1));

// functions.cfm

function f(required numeric x){
    return x;
}

On ColdFusion 11, this actually works fine! I was surpised by that. I was expecting it to simply look in the CFC itself, not see the f() method and get all whiny, producing something like this:

CFC blogExamples.cfml.interfaces.mixin.UsingMixin does not implement the interface blogExamples.cfml.interfaces.mixin.I.

The f method is not implemented by the component or it is declared as private.

I was kinda expecting this to be a compile time thing, but it is checked at runtime, as the next bit of the error reveals:

Stack Trace
at cfusingMixin2ecfm1026091683.runPage(C:/blogExamples/cfml/interfaces/mixin/usingMixin.cfm:1)
coldfusion.runtime.InterfaceRuntimeExceptions$CFCDoesNotImlpementFunctionException:
CFC blogExamples.cfml.interfaces.mixin.UsingMixin does not implement the interface
blogExamples.cfml.interfaces.mixin.I.
 at coldfusion.runtime.InterfaceRuntimeExceptions.throwCFCDoesNotImlpementFunctionException
            (InterfaceRuntimeExceptions.java:19)
 at coldfusion.runtime.TemplateProxy.verifyInterfaceMethodImpl(TemplateProxy.java:1194)
 at coldfusion.runtime.TemplateProxy.verifyInterfaceImpl(TemplateProxy.java:1252)
 at coldfusion.runtime.TemplateProxy.verifyInterfacesImpl(TemplateProxy.java:1054)
 at coldfusion.cfc.ComponentProxyFactory.getProxy(ComponentProxyFactory.java:62)
 at coldfusion.runtime.CFPage.___createObjectInternal(CFPage.java:11198)
 at coldfusion.runtime.CFPage._createObject(CFPage.java:11183)
 at cfusingMixin2ecfm1026091683.runPage(C:\blogExamples\cfml\interfaces\mixin\usingMixin.cfm:1)
 at coldfusion.runtime.CfJspPage.invoke(CfJspPage.java:246)

It's quite odd that it's erroring in usingMixin.cfm rather than in the CFC though. Still: good on ColdFusion for working.

On Lucee I get this:

Lucee 4.5.1.003 Error (expression)
Messagecomponent [C:\blogExamples\cfml\interfaces\mixin\UsingMixin.cfc] does not implement the function [f(numeric x)] of the interface [C:\blogExamples\cfml\interfaces\mixin\I.cfc]
StacktraceThe Error Occurred in
C:\blogExamples\cfml\interfaces\mixin\UsingMixin.cfc: line 1 
1: component implements="I" {
2: include "functions.cfm";
3: }

Well at least it's erroring on the CFC, not the CFM, but remember that this works on ColdFusion. Lucee has some room to improve here. I'll raise a bug when I get done writing this article.

I wanted to see exactly how much I could push the envelope with making the CFC fulfil the interface at runtime, so need tried using a dynamic path, which I pass into the CFC's constructor:

// UsingRuntimeMixin.cfc

component implements="I" {

    function init(required string mixinFile){
        include mixinFile;
    }
}

// usingRuntimeMixin.cfm

o = new UsingRuntimeMixin("functions.cfm");
writeOutput(o.f(1));

Now... if not for UsingRuntimeMixin.cfc needing to implement I.cfc, this code works. So the actual logic is fine. But with the interface requirement put in, now even CF baulks at it, with that same old error again. Which is weird because the function clearly is there, and can be called by the calling code, but it doesn't somehow meet the interface requirement. I am calling this a bug. Lucee doesn't work.

BTW, I tried this approach because I was thinking about the idea of dependency injecting mixins. Which might be useful when testing.

Next I didn't both with the constructor, I just used a request-scope variable to hold the runtime location of the mixin file. I expected this to fail too:

// UsingRequestVariable.cfc

component implements="I" {
    include request.functions;
}

// usingRequestVariable.cfm

request.functions = "functions.cfm";
o = new UsingRequestVariable();
writeOutput(o.f(1));

But this works! Well on ColdFusion it does. None of these slightly-odd versions work on Lucee.

So... how come the request-variable version works, but the "pass the value to the constructor" version not work? I concluded that the interface check is being done after the object is created, but before the constructor is called! This is a bit dumb. It should only be getting called when the actual method comes to be being called: that's the only time it actually matters if the method has been implemented or not.

Update

I've scratched that last sentence because whilst it is precisely what I meant to write at the time, it's a bit vague. I'll try again.

At the end of the day, the proof is in the pudding regarding code requirements is whether the code will run. The function only really needs to be there at the time it comes to be needed (ie: when the function is called, as per above). However this is an invalidation of the concept of interfaces as being appropriate in a dynamic language at all. If we do have an interface in force, then the checking shouldn't be left that late in the piece for the reasons Sean cites in his comment below. However it also shouldn't be checked immediately upon object creation either because CFML is supposed to be a dynamic language, and objects can be manipulated between creation-time and use-time (also elaborated-upon in my comment below).


Hmmm, well that was slightly interesting. I'll get some bugs raised with various bodies (Lucee: 168; ColdFusion: 3940947), and cross ref the tickets back here.

I'm quite pleased ColdFusion will let one use mixins to fulfil interface requirements though. Even if just by a hard-coded include. That's a start.

[pause]

Blimey. I'm staying at my folks' place in Auckland, where they live in a fairly open-plan sort of suburban neighbourhood, but one that used to be pretty bushy (I don't mean like "shrubs", I mean bush: undeveloped low-level wooded area) before all the properties were subdivided. There's still a fair chunk of bush around between the houses. I have been hearing that there's some sort of critter living in the bush outside my room for the last coupla nights, and I just spotted what it was: a thumping great rat, about 30cm long (that's just the body). I was kinda hoping it was a hedgehog or a possum or something mildly cute. But a frickin' rat. Bleah.

I thought I'd share that with you.

Righto.

--
Adam