G'day:
There was a conversation on the CFML Slack channel the other day about mixing-in functions into objects in CFML. This stemmed from some of the way CFWheels has been architected, such as how the main Controller class is composed:
component output="false" displayName="Controller" {
include "controller/functions.cfm";
include "global/functions.cfm";
include "view/functions.cfm";
include "plugins/standalone/injection.cfm";
if (
IsDefined("application") && StructKeyExists(application, "wheels") && StructKeyExists(
application.wheels,
"viewPath"
)
) {
include "../#application.wheels.viewPath#/helpers.cfm";
}
}
I hasten to add that I think this is a questionable design approach - and one I would never personally use or advocate. The only library of functions that belongs in this class is the controller/functions.cfm ones; clearly the ones in the vaguely-named global/functions.cfm don't belong in a class called Controller, and it's even worse that view functions are being injected into a controller class. I can't even speak to what functionality is in global/functions.cfm, but I suspect it's just a bunch of stuff that was left over once everything else found its way in a properly named/designed library. But anyhow, it should be its own class, and injected into the Controller object compositionally, via dependency injection or something. Similarly a controller should not be polluted with view methods, but if a controller needs to call methods on a view object - entirely reasonable - then that view object should also be a dependency of the controller. Ugh.
But anyway. CFWheels is where it is, and this is how it does things, and hence the topic of mixins.
To back up a step, mixins are a strategy of code reuse; basically if used cautiously they can be used to effect a poor-person's implementation of multiple inheritance in languages that don't support it, or a kind of shonky half-baked implementation of the dependency inversion principle. You can probably tell I don't like the idea. I much prefer the stategy of eschewing inheritance (multiple, single or otherwise) wherever possible in favour of a composition strategy - as implemented by dependency injection.
Some languages have formal language constructs for effecting "mixing in". I've seen traits being used in PHP before. Here's a quick example:
trait MyExcellentLib {
function doExcellentThing() {
echo "excellent";
}
}
trait MyCoolLib {
function applyCoolness() {
echo "cool";
}
}
class MyModel {
use MyExcellentLib;
use MyCoolLib;
function executeSomeStuff() {
$this->doExcellentThing();
$this->applyCoolness();
}
}
$model = new MyModel();
$model->executeSomeStuff();
And Ruby does similar with modules. Here's an analogous example of the PHP one in Ruby:
module MyExcellentLib
def doExcellentThing
puts "excellent"
end
end
module MyCoolLib
def applyCoolness
puts "cool"
end
end
class MyModel
include MyExcellentLib
include MyCoolLib
def executeSomeStuff
doExcellentThing
applyCoolness
end
end
model = MyModel.new
model.executeSomeStuff
And CFML's version would be much the same as Ruby's; including .cfm files, as per the CFWheels example above. The problem is one can only include CFML script files (I don't mean "CFScript", I mean scripts (.cfm) as opposed to components (.cfc)). One cannot go include path.to.MyComponent or include "/path/to/MyComponent.cfc". And one certainly cannot include an object. To mixin an object one would need to do it at runtime, not compile time.
This got me thinking, and I decided to write some experimental code to see what I could do with this. If anything. And as-always, I'm TDDIng the exercise.
Compile-time implementation of mix-ins: it mixes-in the required functions
As a baseline, I'm just gonna test the include approach to doing this. Here's the test:
import cfmlInDocker.miscellaneous.mixins.compileTime.MyModel;
component extends=testbox.system.BaseSpec {
function run() {
describe("Testing mixin proofs of concept", () => {
describe("Tests compile-time implementation of mix-ins", () => {
it("mixes-in the required functions", () => {
model = new MyModel()
result = model.executeSomeStuff()
expect(result).toBe("ExcellentCool")
})
})
})
}
}
And the code that fulfils the test:
// MyModel.cfc
component {
include "./myCoolLib.cfm";
include "./myExcellentLib.cfm";
function executeSomeStuff() {
return doExcellentThing() & applyCoolness()
}
}
<cfscript> // myCoolLib.cfm
function applyCoolness() {
return "Cool"
}
</cfscript>
<cfscript>// myExcellentLib.cfm
function doExcellentThing() {
return "Excellent"
}
</cfscript>
(In all cases here, the test passes with the given code, so I'll spare you saying "and that works" each time)
Baseline done. What I want to do now is to do the equivalent with an object, not an include. And at runtime.
Run-time simple implementation of mix-ins: it mixes-in the required functions
it("mixes-in the required functions", () => {
di = new simple.DependencyInjectionImplementor()
model = new simple.MyModel()
di.wireStuffIn(model, new simple.MyExcellentLib())
di.wireStuffIn(model, new simple.MyCoolLib())
result = model.executeSomeStuff()
expect(result).toBe("ExcellentCool")
})
Now I have a DependencyInjectionImplementor class that handles all the wiring-up of things:
// DependencyInjectionImplementor.cfc
component {
function wireStuffIn(someObject, someMixin) {
someObject.__putVariable = putIntoVariables
structKeyArray(someMixin).each((methodName) => {
someObject.__putVariable(someMixin[methodName], methodName)
})
structDelete(someObject, "__putVariable")
}
function putIntoVariables(value, key){
variables[key] = value
}
}
// MyModel.cfc
component {
function executeSomeStuff() {
return doExcellentThing() & applyCoolness()
}
}
// MyExcellentLib.cfc
component {
function doExcellentThing() {
return "Excellent"
}
}
// MyCoolLib.cfc
component {
function applyCoolness() {
return "Cool"
}
}
It just takes and object, loops over its exposed methods, and pops them into the variables scope of the target object.
All those source code files are in /miscellaneous/mixins/runtime/simple.
Job done, but it's a bit basic. Let's keep going…
It mixes-in a subset of functions from a lib
What say one only wants to mixin a subset of the methods from the object? Pretty straight forward:
it("mixes-in a subset of functions from a lib", () => {
di = new advanced.DependencyInjectionImplementor()
model = new advanced.MyModel()
di.wireStuffIn(model, new advanced.MyBrilliantLib(), ["radiateBrilliance"])
result = model.executeSomethingBrilliant()
expect(result).toBe("brilliance")
expect(() => model.failAtDoingItBrilliantly()).toThrow(type="expression")
})
// DependencyInjectionImplementor.cfc
component {
function wireStuffIn(someObject, someMixin, mixinMap) {
someObject.__putVariable = putIntoVariables
functionsToMixin = arguments.keyExists("mixinMap")
? mixinMap
: structKeyArray(someMixin)
functionsToMixin.each((methodName) => {
someObject.__putVariable(someMixin[methodName], methodName)
})
structDelete(someObject, "__putVariable")
}
function putIntoVariables(value, key){
variables[key] = value
}
}
// MyModel.cfc
component {
// ...
function executeSomethingBrilliant() {
return radiateBrilliance()
}
function failAtDoingItBrilliantly() {
return doItBrilliantly()
}
}
// MyBrilliantLib.cfc
component {
function radiateBrilliance() {
return "brilliance"
}
function doItBrilliantly() {
return "done brilliantly"
}
}
(That source code is at /src/miscellaneous/mixins/runtime/advanced).
We can pass in an array of method names, and loop over that if so. Otherwise just continue to loop over the whole object as per before.
Note that because I've not mixed-in doItBrilliantly, we should expect it to error if we then call it.
It should go without saying that any earlier tests I have written continue to pass as I build new functionality into this. Such is the benefit of TDDing… I have that safety net.
It remaps function names if the map specifies it
The next task I set myself is to allow the mixinMap to specify a new name for the mixed-in method, should for some reason one want to do that (avoid naming collisions or something).
it("remaps function names if the map specifies it", () => {
di = new advanced.DependencyInjectionImplementor()
model = new advanced.MyModel()
di.wireStuffIn(
model,
new advanced.MyBrilliantLib(),
{
"radiateBrilliance" = {target="shine"},
"doItBrilliantly" = {}
}
)
result = model.performBrilliantThings()
expect(result).toBe("brilliance done brilliantly")
})
// DependencyInjectionImplementor.cfc
component {
function wireStuffIn(someObject, someMixin, mixinMap) {
someObject.__putVariable = putIntoVariables
arguments.keyExists("mixinMap")
? mixinUsingMap(someObject, someMixin, mixinMap)
: mixinUsingMixin(someObject, someMixin)
structDelete(someObject, "__putVariable")
}
private function mixinUsingMixin(someObject, someMixin) {
structKeyArray(someMixin).each((methodName) => {
someObject.__putVariable(someMixin[methodName], methodName)
})
}
private function mixinUsingMap(someObject, someMixin, mixinMap) {
mixinMap.each((sourceMethod, mapping) => {
targetMethod = mapping.keyExists("target") ? mapping.target : sourceMethod
someObject.__putVariable(someMixin[sourceMethod], targetMethod)
})
}
function putIntoVariables(value, key){
variables[key] = value
}
}
// MyModel.cfc
component {
// ...
function performBrilliantThings() {
return shine() & " " & doItBrilliantly()
}
}
This looks a bit more complicated, but I've just separated-out the two mixing-in methods into their own functions now, to make things more clear.
It handles public/private method access on the mixed-in function
This is more interesting. Now I'm allowing the map to specify whether to mix-in the method as being private or public in the target. Previously they were always private.
it("handles public/private method access on the mixed-in function", () => {
di = new advanced.DependencyInjectionImplementor()
model = new advanced.MyModel()
di.wireStuffIn(
model,
new advanced.MyBestLib(),
{
improveGoodness = {target="makeItBetter"},
makeBest = {access="public"}
}
)
result = model.executeSomeOtherStuff()
expect(result).toBe("better best")
expect(() => model.makeItBetter()).toThrow(type="expression")
})
// DependencyInjectionImplementor.cfc
component {
function wireStuffIn(someObject, someMixin, mixinMap) {
arguments.keyExists("mixinMap")
? mixinUsingMap(someObject, someMixin, mixinMap)
: mixinUsingMixin(someObject, someMixin)
}
private function mixinUsingMixin(someObject, someMixin) {
someObject.__putVariable = putIntoVariables
structKeyArray(someMixin).each((methodName) => {
someObject.__putVariable(someMixin[methodName], methodName)
})
structDelete(someObject, "__putVariable")
}
private function mixinUsingMap(someObject, someMixin, mixinMap) {
someObject.__getVariables = getVariables
scopes = {
public = someObject,
private = someObject.__getVariables()
}
structDelete(someObject, "__getVariables")
mixinMap.each((sourceMethod, mapping) => {
targetMethod = mapping.keyExists("target") ? mapping.target : sourceMethod
targetAccess = mapping.keyExists("access") ? mapping.access : "private"
scopes[targetAccess][targetMethod] = someMixin[sourceMethod]
})
}
function getVariables(){
return variables
}
function putIntoVariables(value, key){
variables[key] = value
}
}
// MyModel.cfc
component {
// ...
function executeSomeOtherStuff(){
return variables.makeItBetter() & " " & this.makeBest()
}
}
// MyBestLib.cfc
component {
function improveGoodness() {
return "better"
}
function makeBest() {
return "best"
}
}
It ignores access modifiers other than public / private (falling back to private)
I introduced a small bug into that last implementation. I could specify any access level I wanted, whereas the only valid access levels here are public or private. I need to fix this.
it("ignores access modifiers other than public / private (falling back to private)" , () => {
di = new advanced.DependencyInjectionImplementor()
model = new advanced.MyModel()
di.wireStuffIn(
model,
new advanced.MyBestLib(),
{
makeBest = {access="INVALID"}
}
)
result = model.checkIfItsTheBest()
expect(result).toBe("best")
expect(() => model.makeBest()).toThrow(type="expression")
})
private function mixinUsingMap(someObject, someMixin, mixinMap) {
// ...
mixinMap.each((sourceMethod, mapping) => {
// ...
requestedAccess = mapping.keyExists("access") ? mapping.access : "private"
targetAccess = requestedAccess == "public" ? "public" : "private"
scopes[targetAccess][targetMethod] = someMixin[sourceMethod]
})
}
I could have rolled that into one line, but it was getting a bit awkward-looking, so I figured two lines - each with clear labels - were clearer in intent.
Oh! And the model file:
component {
// ...
function checkIfItsTheBest(){
return variables.makeBest()
}
}
Back up in the test I just make sure that it's only available in the private scope.
End of round 1
That's where I got to yesterday when writing the code for all this. I was discussing this with Tom King (lead contributor on CFWheels), and he observed one flaw in the way I'm doing these mixins is that they lose the context of the object they were originally homed in. Because of how CFML handles injecting methods by reference like how I am doing here, once the method is in the target object, any references that method makes to variables or this is a reference to the target object, not the source object that they came from. My initial reaction to this was "well: yes", but I'm only thinking of mixing functions from function libraries here. There's also a usecase for a mix-in library to also require its own context, so I need to address this too. I worked out how to do this pretty quickly (which made me pleased with myself, I have to admit), but I figured I need to verify the behaviour some more before I am happy with it, plus also I'm now thinking that how these contexts are bound should be perhaps optional. And I already have a lot of content in this article already, so I'll work on the next wodge of ideas some more before writing them up. For now I'm gonna call it quits, have a beer, and play dumb-arse games for the balance of the evening. Ooh: and eat! It's eating time.
(I've just written Part 2 of this exercise).
Righto.
--
Adam