Friday 15 November 2013

Synthesised accessors & variable scope

G'day:
Whilst vegetating at home last night, I had a look at some code Ryan Guill had posted on the ColdFusion IRC channel. It relates to ColdFusion's ability to automatically create accessors on CFC properties, if told to do so. He was seeing some "unexpected" behaviour.

My exposure to this stuff is pretty superficial: mostly just testing. So excuse me please if my investigations here are somewhat mundane and unsurprising.

OK, so Ryan had a situation in which he had accessors switched on for a CFC, but as well had actually specifically implemented a private setter for a property:

// WithPrivateSetter.cfc
component accessors=true {

    property myProperty;

    function init(myProperty){
        setMyProperty(myProperty);
        return this;
    }

    private void function setMyProperty(myProperty){
        variables.myProperty = "set via explicit private setter: " & arguments.myProperty;
    }

}

When this was called, we get what were unexpected results for Ryan:

// testWithPrivateSetter.cfm

o = new WithPrivateSetter("set via constructor");
writeDump(var=o, label="After init()");
writeOutput("<hr>");

o.setMyProperty("set via synthesised setter");
writeDump(var=o, label="After setMyProperty()");

After init() - component scribble.shared.git.blogExamples.components.synthesisedAccessors.WithPrivateSetter
PROPERTIES
myPropertyset via explicit private setter: set via constructor
METHODS

After setMyProperty() - component scribble.shared.git.blogExamples.components.synthesisedAccessors.WithPrivateSetter
PROPERTIES
myPropertyset via synthesised setter
METHODS

What was unexpected was that the external call worked at all, as he was expecting the explicit private method to overload the synthesised one. The basis for this is that this is what happens if one specifies an explicit public setter, eg:

// WithPublicSetter.cfc
component accessors=true {

    property myProperty;

    function init(myProperty){
        setMyProperty(myProperty);
        return this;
    }

    public void function setMyProperty(myProperty){
        variables.myProperty = "set via explicit public setter: " & arguments.myProperty;
    }

}

(I'll skip the calling code this time, as it's much the same)

After init() - component scribble.shared.git.blogExamples.components.synthesisedAccessors.WithPublicSetter
PROPERTIES
myPropertyset via explicit public setter: set via constructor
METHODS

After setMyProperty() - component scribble.shared.git.blogExamples.components.synthesisedAccessors.WithPublicSetter
PROPERTIES
myPropertyset via explicit public setter: set via synthesised setter
METHODS

So because there's an explicitly declared setter, the synthesised one doesn't get created (and just as well, as it would suck, otherwise).

We're left wondering why the private setter in the first example doesn't similarly "block" the synthesised one from being created?

However if one thinks about it, this makes perfect sense. Think about variable scopes here. A public-access method is in the object's this scope, and private-access methods are in the object's variables scope. Given that, the synthesised public-access setter is no more gonna "overload" (or more to the point: replace) the private-access one than expecting these variables to interfere with each other:

this.myVar = "public";
variables.myVar = "private";

Those are - clearly - two different variables. It's the same with the functions.

I think the confusion comes about perhaps is because we're not used to being able to do this with functions. We can't declare two functions with the same name and different access levels:

// BothTypes.cfc
component {
    private function f(){
        
    }    
    public function f(){

    }    
}

This doesn't compile, and gives this familiar error:

Routines cannot be declared more than once.

The routine f has been declared twice in the same file.The CFML compiler was processing:

  • A script statement beginning with public on line 6, column 9.
The error occurred inC:/Apps/Adobe/ColdFusion/10/cfusion/wwwroot/scribble/shared/git/blogExamples/properties/synthesisedAccessors/BothTypes.cfc: line 6
4 :   
5 :  } 
6 :  public function f(){
7 : 
8 :  }

No surprises there.

With ColdFusion 10 (and Railo 4.x) we can actually work around this using function expressions instead of function statements:

// BothTypesViaExpressions.cfc
component {

    variables.f = function(){
        writeOutput("private method<br>");
    };

    this.f = function(){
        writeOutput("public method<br>");
    };

    function proxy(){
        variables.f();
        this.f();
        f();
    }

}

// testBothTypesViaExpressions.cfm

o = new BothTypesViaExpressions();

writeOutput("Call public method:<br>");
o.f();
writeOutput("<hr>");

writeOutput("Call both methods via proxy:<br>");
o.proxy();

Yields:

Call public method:
public method


Call both methods via proxy:
private method
public method
private method


So that's a good technique to remember, and also approaching functions from that angle more-clearly demonstrates why Ryan was seeing what he was seeing.

I've got more to say on this topic, but I'm running out of lunchtime, so I'll break here.

If you're wondering where my next TDD / unit testing article is, it's well under way, and I have a bunch of time on trains, airports, planes, B&B rooms & pubs over the weekend as I'm off to Ireland soon to see my boy. I'll get it finished before my return flight on Sunday, I think. Stay tuned...

--
Adam