Tuesday 21 October 2014

A quick look at DI/1

G'day:
I'm at CFCamp at the moment, and one of the better presentations yesterday (out of a lot of good presentations, I might add) was Chris' presentation, "Dependency Injection with DI/1". It looked sufficiently interesting I thought I'd have a quick look-see at it. I have downloaded it in the past, but that's about as far as I had got.

I've been using ColdSpring at work for the last few years, but DI has come a long way since ColdSpring's heyday. So I'm familiar with the concepts of dependency injection, but only ColdSpring's implementation of it.

If you don't know what DI is, go read up on it first otherwise none of this will be very meaningful. I'm not able to get an internet connection at the moment, so I can't find a decent link for you, but I'll try to remember to update this later on. Or, hey, you probably know how Google works.



OK, so DI/1 lives here: https://github.com/framework-one/di1. Installation is simply to clone the repo. Or indeed it's just one file (yeah, one file), so simply grab ioc.cfc and chuck it somewhere. I've put it in /shared/frameworks/di1, and I map it in in Application.cfc:

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

    function onApplicationStart(){
        application.beanFactory = new di1.IOC(folders=expandPath("./api"))
    }

}

That's all the configuration one needs to do, if one is able to organise one's files the way DI/1 expects them to be.

In my API directory I have a coupla CFCs:

// SomeService.cfc
component {

    echo("SomeService pseudoconstructor called<br>")

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

    function getVariables(){
        return variables
    }
}

// SomeDao.cfc
component {

    echo("SomeDao pseudoconstructor called<br>")

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

}


And if I want a SomeService object, I just have to grab the bean:

//testSimple.cfm

echo("testSimple.cfm, before creating someService<br>")
someService = application.beanFactory.getBean("SomeService")

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

dump(var=someService, label="someService")
dump(var=someService.getVariables(), label="someService variables scope")

DI/1 handles the wiring-up of the DAO automatically. Cool.

One thing to be mindful of here is that DI/1 maps the dependencies via the argument name not the argument type. Initially my SomeService.cfc's init()'s method signature was like this:

function init(required SomeDao dao)

To which DI/1's response was:

bean not found: dao; while resolving constructor arguments for SomeService

I'm not sure what to think about that. I s'pose an argument will always have a name specified, but not necessarily a type. However I think the dependency here is that it's a SomeDAO object; not that the method happens to call it someDao.

BTW, the output for the first hit of that code is thus:


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




someService
Component (shared.scratch.blogExamples.frameworks.di.di1.newapp.api.SomeService) 
Only the functions and data members that are accessible from your location are displayed
public
INITvar.INIT
GETVARIABLESvar.GETVARIABLES
someService variables scope
Variable Scope (of Component)
GETVARIABLESvar.GETVARIABLES
INITvar.INIT
someDao
Component (shared.scratch.blogExamples.frameworks.di.di1.newapp.api.SomeDao) 
Only the functions and data members that are accessible from your location are displayed
public
INIT
Public Function init
source:SomeDao.cfc
arguments
labelnamerequiredtypedefaulthint
return typeany
THISvar.THIS

On second hit, DI/1 has already done all its stuff, so we get this:


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


So all the dependent bean config & creation happens once on application start-up.

This is all fine if you can implement your code in the way DI/1 wants it to be. But how about if you're working with an existing application which wasn't architected with DI/1 in mind?

I've just knocked together a quick "app" which implements a user login sort of arrangement. There's a bunch of code, but it's all pretty simple. Don't worry about memorising the code... there are notes on the important bits @ the bottom:

// UserService.cfc
component {

    public UserService function init(required IUserDAO userDAO, required Logger auditLog){
        structAppend(variables, arguments)
        return this
    }

    public User function getUserById(required numeric id){
        var userRecord = userDAO.getUserById(id)
        if (userRecord.recordCount){
            var user = new User()
            setFromRecord(user, userRecord)
            return user
        }else{
            throw(type="InvalidUserException", message="User does not exist", detail="No user with ID #id# was found")
        }
    }

    public boolean function authenticate(required string loginId, required string password){
        var userRecord = userDAO.getUserByLogin(loginId, password)
        if (userRecord.recordCount){
            setFromRecord(user, userRecord)
            return true
        }else{
            auditLog.logEntry("Login attempt failed for user #loginId#, pwd #password#")
            return false
        }

    }

    private void function setFromRecord(required User user, required query record){
        user.setId(record.id)
        user.setFirstName(record.firstname)
        user.setLastName(record.lastName)
        user.setLoginId(record.loginId)
    }

}


// IUserDao.cfc
interface {

    public query function getUserById(required numeric id);
    public query function getUserByLogin(required string loginId, required string password);

}


// MockedUserDAO.cfc
component implements="IUserDAO" {

    records = queryNew(
        "id,firstName,lastName,loginId,password",
        "integer,varchar,varchar,varchar,varchar",
        [
            [1, "Zinzan", "Brooke", "number", "eight"],
            [2, "Daniel", "Vettori", "leftarm", "spin"]
        ]
    )

    public MockedUserDAO function init(required Logger transactionLog){
        structAppend(variables, arguments)
        return this
    }

    public query function getUserById(required numeric id){
        transactionLog.logEntry("getUserById(#id#) called")
        return queryExecute("SELECT * FROM records WHERE id = :id", {id=id}, {dbtype="query", records=records})
    }

    public query function getUserByLogin(required string loginId, required string password){
        transactionLog.logEntry("getUserByLogin('#loginId#', 'REDACTED') called")
        return queryExecute("SELECT * FROM records WHERE loginId = :loginId AND password = :password", {loginId=loginId,password=password}, {dbtype="query", records=records})
    }

}


// Logger.cfc
interface {
    public void function logEntry(required string text, string severity);
}


// TransactionLog.cfc
component implements="Logger" {

    public TransactionLog function init(required string logFile){
        structAppend(variables, arguments)
        return this
    }

    public void function logEntry(required string text, string severity="information"){
        writeLog(file=logFile, text=text, type=severity)
    }
    
}


// AuditLog.cfc
component implements="Logger" {

    public AuditLog function init(required string logFile, required Encrypter encrypter){
        structAppend(variables, arguments)
        return this
    }

    public void function logEntry(required string text, string severity="warning"){
        writeLog(file=logFile, text=encrypter.encrypt(text), type=severity)
    }

}


// Encrypter.cfc
interface {
    public string function encrypt(required string string);
    public string function decrypt(required string string);
}


// StubEncrypter.cfc
component implements="Encrypter" {

    prefix = "ENCRYPTED_"

    public string function encrypt(required string string){
        return "#prefix##string#"
    }
    public string function decrypt(required string string){
        return reReplace(string, "^#prefix#", "", "ONE")
    }

}


// User.cfc
component accessors=true {

    property id;
    property firstName;
    property lastName;
    property loginId;
    property name="isAuthenticated" default=false;

    public User function init() {
        return this
    }

    public function getAsStruct(){
        return {
            id                = id,
            firstName        = firstName,
            lastName        = lastName,
            loginId            = loginId,
            isAuthenticated    = isAuthenticated
        }
    }

}


Basically:

  • there's a user service which contains some business logic around getting a user, and authenticating them.
  • It takes a DAO for DB access. Here's a reasonable use of specifying an interface rather than a concrete type: I'm actually not using a real DAO here, I'm using a mock, but it implements the same interface as the service is expecting.
  • The DAO logs its activity.
  • Also the authentication process logs some info if authentication fails, this log encrypts some of the data it.
  • For the hell of it, I'm using a coupla interfaces in there as well (this had more potential relevance before I realised DI/1 does its wiring via argument name, rather than type).


Don't pay too much attention to what the code here is doing and how I'm doing it. That is not the point here. I just chucked some stuff together to implement different edge cases that I wanted to see how to handle with DI/1.

Firstly, here's an example of creating a UserService, and using it:

// testUserServiceManually.cfm

userService = new theapp.users.UserService(
    userDAO        = new theapp.users.MockedUserDAO(
        transactionLog = new theapp.loggers.TransactionLog(logFile="theappTransactionLog")
    ),
    auditLog    = new theapp.loggers.AuditLog(
        logFile        = "theappAuditLog",
        encrypter    = new theapp.security.StubEncrypter()
    )
)


user = userService.getUserById(1)
dump(user.getAsStruct())


That's a bit of a mouthful.

The main stumbling block for DI/1 here is that my argument names don't have a one-to-one mapping to the data types of the arguments. This will be a fairly common thing, I would have thought? But DI/1 cannot infer how to wire all this up, so I need to give it a hand working all this out. It's surprisingly easy:

// Application.cfc
component {
    this.name        = "di1_1"
    this.mappings    = {
        "/di1"        = expandPath("/shared/frameworks/di1"),
        "/theapp"    = expandPath("./me/adamcameron/theapp")
    }
    this.applicationTimeout = createTimespan(0,0,0,10)

    function onApplicationStart(){
        application.beanFactory = new di1.IOC(folders=expandPath("./me/adamcameron/theapp"))
        configureDi()
    }

    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",
            encrypter    = application.beanFactory.getBean("encrypter")
        })
    }

}

And that's it. I'm doing a few different things here:

  • using declareBean() to force the mapping between my userDAO argument and the mocked DAO component
  • using addBean() to actually create the TransactionLog bean. I could have declared it, but I couldn't be bothered having to horse around working out how to get the logFile value into it.
  • demonstrating declaring a bean, then using its reference in a subsequent bean declaration.

So now here's the code using the UserService:

// testUserServiceDI.cfm

userService = application.beanFactory.getBean("UserService")

user = userService.getUserById(1)

loggedIn = userService.authenticate(user.getLoginId(),"INVALID_PASSWORD")

dump([user.getAsStruct(), loggedIn])


And when we run this, we get:

Array
1
Struct
firstName
stringZinzan
id
number1
isAuthenticated
stringfalse
lastName
stringBrooke
loginId
stringnumber
2
booleanfalse

And in the transaction log:

"INFO","qtp1599915876-16","10/21/2014","12:35:26","","getUserById(1) called"
"INFO","qtp1599915876-16","10/21/2014","12:35:26","","getUserByLogin('number', 'REDACTED') called"

And the audit log:

"WARN","qtp1599915876-16","10/21/2014","12:35:26","","ENCRYPTED_Login attempt failed for user number, pwd INVALID_PASSWORD"


There's nothing revolutionary in my experimentation here, but it has demonstrated to me that DI/1 is really pretty easy to use, and it also confirmed that one doesn't need to rely on its "convention over configuration" approach, which is an apprehension I had initially.

Righto... time for lunch.

--
Adam

PS: apologies for the quality of the writing today... I'm a bit under the weather after being out at the CFCamp dinner last night... mixing ouzo and beer for six hours (!) until 1:30am this morning at the local Greek restaurant has left me somewhat "jaded" today.