Monday 27 October 2014

A quick look at WireBox

G'day:
A coupla days ago I had "A quick look at DI/1", after having my interest piqued by Chris' presentation "Dependency Injection with DI/1". That went quite smoothly, so to kill some time after CFCamp, I decided to do the equivalent exercise with WireBox.

I performed exactly the same two exercises: first just create some CFCs and and wire them together however the framework in question suggested, then secondly wire together an existing application, in which I cannot make any pervasive changes to the code (say it's someone else's code or something).

Firstly Brad's quick reference card is pretty handy, and all I needed for my first exercise. It's here: "WireBox RefCard".

Wiring code written from scratch

The code I am wiring together is the same as last time: SomeService.cfc which takes SomeDao.cfc as a dependency. I init Wirebox as follows:

// Application.cfc
component {
    this.name        = "wirebox_16"
    this.mappings    = {
        "/wirebox"    = expandPath("/shared/frameworks/wirebox/wirebox"),
        "/api"        = expandPath("./api")
    }
    this.applicationTimeout = createTimespan(0,0,0,10)

    function onApplicationStart(){
        wirebox = new wirebox.system.ioc.Injector()
    }

}

Note I don't set it in the application scope: this is done internally to WireBox: it creates application.wirebox automatically. I don't like that much, TBH (for the classical encapsulation reasons). And I don't see how it's beneficial, or worth their while doing. Still: it's easy to configure. There's plenty of other options once can initialise it with, but I did not need anything else for this exercise.

Next the CFCs:

// SomeService.cfc
component {

    echo("SomeService pseudoconstructor called<br>")

    function init(required SomeDao someDao inject="api.SomeDao"){
        echo("SomeService init() called<br>")
        variables.someDao = arguments.someDao
        return this
    }

    function getVariables(){
        return variables
    }
}

Note I've had to annotate my code here. WireBox doesn't seem to have the capability to infer the wiring from the argument names like DI/1 can. Or at least I couldn't work out how to do it. I think it's completely inappropriate to put DI-framework-specific code into my code, btw, but this is just how WireBox works.

I did not have to do anything to SomeDao.cfc:

// SomeDao.cfc
component {

    echo("SomeDao pseudoconstructor called<br>")

    function init(){
        echo("SomeDao init() called<br>")
        return this
    }

}

To then use my service in my calling code, I just had to do this:

//testSimple.cfm

echo("testSimple.cfm, before creating someService<br>")
someService = application.wirebox.getInstance("api.SomeService")

echo("testSimple.cfm, after creating someService<br>")

dump(var=structKeyExists(someService.getVariables(), "someDao"), label="Test the someDao object is in there")

That's easy enough. The output here is as follows:

testSimple.cfm, before creating someService
testSimple.cfm, after creating someService

Test the someDao object is in there
booleantrue

Um... what I'm expecting to see is this:

testSimple.cfm, before creating someService
SomeDao pseudoconstructor called
SomeService pseudoconstructor called
SomeService pseudoconstructor called
SomeDao pseudoconstructor called
SomeDao init() called
SomeService init() called
testSimple.cfm, after creating someService

Test the someDao object is in there
booleantrue

Hmmm. Obviously all the code is being run, but Wirebox is suppressing output on some of it. Whilst this is insignificant - one should not be outputing stuff directly from CFC code - it is not Wirebox's call to enforce this, and it should not be interfering with the "normal" operations of the code it's providing DI for. Remember that that is all Wirebox is supposed to be doing here: dependency injection. I should be doing nothing else. This sort of thing irks me, if I'm honest.

Still: the code to get this to work is OK. It's a bit more ham-fisted than DI/1's approach, that said.

Wiring an existing App

The next exercise I set myself is the same as the one in my DI/1 investigation: I have contrived a small app, and the task is to wire it up. Without modifying the app at all. Firstly one should absolutely not have to monkey with one's own code to effect DI, plus it's a very real possibility that the code concerned might not be one's own code: it could be a third party package. So one shouldn't be monkeying with third-party code just to make DI work; and if the code is precompiled or [blurk] the source is encrypted - although I think only Adobe do that these days - then it's simply not possible.

Here's where WireBox let itself down, and once again demonstrated "more docs" does not in any way equate to "better docs". The *Box docs certainly are voluminous, and a lot of people seem to think this is a good thing, however in my experience the volume is achieved by adding more cruft, not by adding more content. I find their docs to be quite a challenge to work with, and an unpleasant reading experience. The exception here is Brad's reference cards: they stick to the point and articulate just the important stuff.

However I could not see any reference on Brad's WireBox ref card to configuring DI in a non-invasive way, and having resorted to their biblically-proportioned main docs, I couldn't find anything either. I wasn't prepared to actually read the docs as I'd rather open a vein, but I scanned back and forth and just couldn't find the relevant material.

I put it to Twitter, and Dom came to the rescue:

And moments later had a Gist for me:


This also points to the relevant part in the docs which I had missed earlier: "Mapping DSL". My excuses of cruft aside, I really should have spotted that. I can actually recall seeing "Mapping DSL" and looking at the code and wondering why they were referring to it as a DSL (because it's not; it's just "method chaining" which is not the same thing), but didn't actually pay attention to the content. But anyway, Dom's code allowed me to round this example out:

// Application.cfc
component {
    this.name        = "wirebox_1"
    this.mappings    = {
        "/wirebox"    = expandPath("/shared/frameworks/wirebox/wirebox"),
        "/thesite"    = expandPath("."),
        "/theapp"    = expandPath("./me/adamcameron/theapp")
    }
    this.applicationTimeout = createTimespan(0,0,0,5)

    function onApplicationStart(){
        wirebox = new wirebox.system.ioc.Injector("thesite.WireboxBinder")
    }

}

The key part here is the Binder concept, which I pass a path to when calling the injector. There's a slight gotcha in WireBox here as my WireboxBinder.cfc is in the same directory as Application.cfc, so the path should simply be able to be "WireboxBinder", but I could not get that to work. I needed to use a dummy mapping so Wirebox could find it. I suppose this is because my code is not in the root of either the Railo server or the website, so I need to "help" Wirebox find stuff.

My binder file is thus:

// WireboxBinder.cfc
component extends="wirebox.system.ioc.config.Binder" {

    function configure() {
        map("TransactionLog")
            .to("theapp.loggers.TransactionLog").asSingleton().noAutowire()
                .initArg(name="logFile", value="theappTransactionLog")

        map("MockedUserDAO")
            .to("theapp.users.MockedUserDAO").asSingleton().noAutowire()
                .initArg(name="transactionLog", ref="TransactionLog")

        map("StubEncrypter")
            .to("theapp.security.StubEncrypter").asSingleton().noAutowire()

        map("AuditLog")
            .to("theapp.loggers.AuditLog").asSingleton().noAutowire()
                .initArg(name="logFile", value="theappAuditLog")
                .initArg(name="encrypter", ref="StubEncrypter")
        
        map("UserService")
            .to("theapp.users.UserService").asSingleton().noAutowire()
                .initArg(name="userDAO", ref="MockedUserDAO")
                .initArg(name="auditLog", ref="AuditLog")
    }

}

That's not too bad, but it's quite verbose. Compare it to the DI/1 equivalent:

private function configureDi(){
    application.beanFactory.declareBean("userDAO", "theapp.users.MockedUserDAO")
    application.beanFactory.addBean("transactionLog", new theapp.loggers.TransactionLog(logFile="theappTransactionLog"))

    application.beanFactory.declareBean("encrypter", "theapp.security.StubEncrypter")
    application.beanFactory.declareBean("auditLog", "theapp.loggers.AuditLog", true, {
        logFile = "theappAuditLog"
    })
}


I guess WireBox is all about messing with the CFCs themselves and that's the intended way for it to work, so there's a slight cost to implement this approach.

(My application code is identical to that in the DI/1 example, btw, so I'll not repeat it here. It's all homed on GitHub though: di/wirebox/existingapp/me/adamcameron/theapp).

This all works fine, and the output to screen and log is correct (as per the DI/1 example).

Conclusion

DI/1 was easier to implement, was more intuitive and I could do it all without resorting to asking for help (even if the help was simply to be a "documentation look-up service" for me). I find WireBox's default position that one should have to annotate files to make DI to work is... well... fundamentally flawed. If I was working on an application that already used other *Box technology, I'd use WireBox. Other than that: I'd use DI/1. DI/1 just seems to get on with it with as little ceremony or nonsense as necessary, which is how I prefer this sort of thing to work.

Righto.

--
Adam