Saturday 26 June 2021

CFWheels: improving the usability of plugins

G'day:

That's perhaps a slightly strange article title. I was trying to be diplomatic, and anyone who's read anything I write here will know: this is not my strong suit. Hopefully it'll make sense by the time I finish.

I recently did that series on mixins in CFML: "CFML: messing around with mixins (part 1)" (and parts 2 & 3). This all stemmed from trying to make head or tail of an architectural choice CFWheels has made, which has resulted in a lot of mixed-in functions polluting everything (view functions in models, controller functions in views, helper functions all over the place), and a necessity to eschew private functions in favour of using a "special code" that when a function is prefixed with $, then despite it being public yer supposed to pretend it's private and not use it. Umm… so be it.

I had a need to look at what CFWheels did with its "plugins" feature the other day, and was unsurprised these have taken the same route as all its other function libraries: they can't really be nicely encapsulated classes, because all their methods need to be public and the objects can have no state because of how CFWheels has been architected. I'm not going to play the game by these particular rules, so I've looked at how I can write a plugin, but still actually implement it using proper coding practices at the same time.

It turns out it's pretty easy, just leveraging a bit of closure. Here's a simple example:

import cfmlInDocker.dao.MyDao
import cfmlInDocker.models.Thing

component {

    function init() {
        this.version = "2.2"

        variables.dao = new MyDao()

        return this
    }

    this.selectThingById = function (id) {
        record = variables.dao.selectThingById(id)
        if (record.recordCount) {
            return new Thing(record.id, record.name)
        }
        throw(type="ThingNotFoundException", message="Thing not found", detail="Data for Thing with ID #id# not found")
    }

    this.getVersion = function () {
        return this.version
    }

    this.getSecret = function () {
        return variables.getSecret()
    }

    private function selectThingById(id) {
        return variables.dao.selectThingById(id)
    }

    private function getSecret() {
        return "sssshhhh"
    }
}

It all comes down to how I'm publicly exposing the private functions via a this-scoped wrapper function expression, and that uses closure to preserve variable references to the context of this object.

I'm not going to dwell on it here because I cover all this in those other three articles (specifically the second one), but the way CFWheels is injecting plugin functions into everything, the context of the functions is not preserved, so references to variables and this refer to where they've been injected into, not the object they started in. Using closure ensures the context is coupled to the function, and this persists when the function reference is injected into some other object.

I have tests for all this in /test/functional/plugins/MyPluginTest.cfc. I won't repeat them here cos all they're doing is verifying the plugin methods do what I expect them to do. Have a look though.


But I'm still not happy about this.

It might be fine in CFWheels's mind that polluting every object with seemingly every function in the entire application, but I'm a bit more conserative when it comes to the single responsibility principle, and I really don't like the fact that my plugin is polluting everything with three unnamespaced functions: selectThingById, getVersion and getSecret. A sensible application of the concept of a plugin would be to inject an object perhaps. And the object then exposes its methods. All crazy and OOP I know. I didn't quite nail this (although as I type this paragraph, I think I have worked out a better way than what I'm about to show you), but I think this is an improvement of sorts:

import cfmlInDocker.dao.MyDao
import cfmlInDocker.models.Thing
import cfmlInDocker.plugins.myPlugin.MyPlugin

component extends=MyPlugin {

    function init() {
        this.version = "2.2"

        variables.dao = new MyDao()

        return this
    }

    this.MyOtherPlugin = function () {
        return {
             selectThingById = (id) => super.selectThingById(id),
             getVersion = () => super.getVersion(),
             getSecret = () => super.getSecret()
        }
    }
}

In this example, just one unnamespaced function is exposed, and that returns a struct that then exposes methods. I'm just borrowing the actual code from MyPlugin for the functionality here. Note that the references to the actual functions are still wrapped in a function expressions so that closure binds the super references to this object's super. Same as with the earlier example.

Usage of this can be seen in my test controller (/src/controllers/TestRoute.cfc):

function testMyOtherPlugin() {
    myOtherPlugin = MyOtherPlugin()
    viewVariables = {
        thing = myOtherPlugin.selectThingById(params.id),
        version = myOtherPlugin.getVersion(),
        secret = myOtherPlugin.getSecret()
    }

    renderView(template="plugin", values=viewVariables)
}

MyOtherPlugin is the only thing that is exposed from this plugin. After that, its return value behaves like an object. Like, you know, how the rest of us have been writing our CFML code for the last decade and a half.

Righto.

--
Adam