Dependency Injection

Your gateway to loosely coupled and flexible code.

So, what is dependency injection...

Dependency Injection removes what can sometimes be the mess of configuring modules and the resources they need to consume from the normal flow of code. Instead of long constructor functions and complex constructor arguments, configuration is defined in a confie module, and the di container does all the wiring up for you. It removes hard dependencies from code, and configuration can be easily altered for different needs.

Example

When the havok/exception/handler renders an exception to the console, it uses havok/exception/ConsoleLogRenderer. If you write your own improved renderer, you can use it with this simple change to the config:

di: {
    'havok/exception/Handler': {
        gets: {
            consoleLogRenderer: 'my/new/Renderer'
        },
    }
}

Creation

There can be multiple di containers. A di container is created like this:

require(['havok/di/Di'], function(Di){
    var di = new Di({/*my config*/});
}

Config

The behaviour of the di container is controlled by the config object.

The config object can be explicitly passed to the constructor:

var di = new Di({/*put configuration here*/});
The config object can be set after the di container has been created:
di.setConfig({/*put configuration here*/});

If no config object is supplied to the constructor, the di key from the dojo config object will be used.

The a config object can be merged with the existing di config using the mergeConfig method:

di.mergeConfig({/*put config to merge here*/});

The config for an individual identifier can be set with:

di.setIdentifierConfig(identifier, {/* put config here*/});

For docs of the config format, see below.

sharedDi!

Other Havok use one common di container configured with the dojo config object. This allows the di config to be merged by havok/config/manager.

To get the shared di container use the AMD plugin:

require(['havok/di/sharedDi!'], function(sharedDi){
    //do something
}

Retrieving objects

To fetch an object from the di you need to pass the di an identifier. The di will look the identifier up in the config, do any injections, and pass the result back to you. An object can be fetched from the di with three different methods:

name description
di.get(identifier) Simple and clear. It gets an object.
di.create(identifier) Creates an object, ignoring any object already cached with the identifier.
di.proxy(identifier) Gets a proxy, or a stand-in for the object. Good for lazy loading.

The differences between these methods are explained more below.

The property names of the config object are identifiers. They are used to retrieve configured objects from the di. The properies themselves define how the object will be injected. The property object can have the following sub properties. All sub properties are optional:

name type description
base string Specify which module will be the base, or foundation for injection.
params object Specifies static parameters to be injected.
gets object Specifies objects that should be retrieved through the di and injected.
proxies object Specifies object that should be proxied by the di and injected.
directives object A set of flags that give fine grained control over di injection behaviour.
proxyMethods array An array of methods that should be available when creating a proxy.

Tell di which object injections should be applied to.

The base defines which object the di will inject. For example, with this config di.get('myIdentifier') will get an instance of my/module with the color property set to blue.

'myIdentifier': {
    base: 'my/module',
    params: {
        color: 'blue'
    }
},

Ommited Base

If the base is ommitted, the identifier is assumed to be the base. Eg:

'my/module': {
    params: {
        color: 'blue'
    }
},

Chaining

If base is set to a string, the base will be fetched through the di. This allows chaining.

For example, with this config di.get('myIdentifier') will get an instance of my/module with the color property set to blue and size property set to big.

'my/module': {
    params: {
        color: 'blue'
    }
},
'myIdentifier: {
    base: 'my/module',
    params: {
        size: 'big'
    }
}

Identifier without config

If an identifier is not configured, the di will assume it is a module, and just load that module using require.

Object base

If the base is an object, rather than a string, that object will be used for injection.

For example, with this config di.get('myIdentifier') will return {size: 'big'}.

'myIdentifier: {
    base: {},
    params: {
        size: 'big'
    }
}

Scalar values to be injected

Any property set in the params object will be set on the returned object.

For example:

'my/module': {
    params: {
        color: 'blue',
        size: 'big',
        number: 6,
        something: {blah: 4}
    }
}

Objects to be injected

Simple get

Any property set in the gets object will be fetched through the di and then set on the returned object.

For example, with this config di.get('my/zoo') will return an instance of my/zoo populated with one lion and two tigers. The two tigers will have different names.

'my/zoo': {
    gets: {
       lion: 'my/lion/module',
       tiger1: 'tiger1',
       tiger2: 'tiger2
    }
},
'tiget1': {
    base: 'my/tiger/module',
    params: {
        name: 'Toby'
    }
},
'tiget1': {
    base: 'my/tiger/module',
    params: {
        name: 'Alice'
    }
}

Inline config

Rather than specifying a string in a get, you can use an inline config to make the code more consise and easier to read. The example above could be rewritten as:

'my/zoo': {
    gets: {
       lion: 'my/lion/module',
       tiger1: {
            base: 'my/tiger/module',
            params: {
                name: 'Toby'
       },
       tiger2: {
            base: 'my/tiger/module',
            params: {
                name: 'Alice'
       }
    }
}

Objects to proxy, then inject

Proxy objects can be injected with exactly the same synatx as gets, the only difference being a proxy is injected instead of the actual object.

For example:

'my/zoo': {
    proxy: {
       lion: 'my/lion/module',
       tiger1: {
            base: 'my/tiger/module',
            params: {
                name: 'Toby'
       },
       tiger2: {
            base: 'my/tiger/module',
            params: {
                name: 'Alice'
       }
    }
}

For information on proxy object and why you might use them, see the Proxy Objects section.

Fine grained control over fetching objects

There are four possible directives. Each is a boolean flag.

The default directives are:

directives: {
    declare: false,
    define: false,
    cache: true,
    clone: false
};

cache

The di container automatically caches objects. If you want to recreate the object every time di.get() is called, then set cache: false.

For example:

'my/module': {
    directives: {
        cache: false
    }
}

declare

If the declare directive is set to true, then dojo/_base/declare will be used on the injected object, and a contstructor returned rather than an istance. This is especially useful for injecting widgets that are created by the parser. eg if this config is set:

'object1': {
    base: 'dijit/Form/TextBox',
    directives: {
        declare: true
    },
    params: {
        a: 1
    }
}

Then this markup could be used:

<script type="text/javascript">
    require([
        'dojo/parser',
        'get!object1',
    ],
    function(parser){
        parser.parse()
    })
</script>

<div data-dojo-type: 'object1'></div>

This would result in a TextBox with a = 1.

define

If define is set to true, a new AMD module is defined with the identifier. eg, if this config is set:

'object1': {
    base: 'My/Module',
    directives: {
        define: true
    },
    params: {
        a: 1
    }
}

The the following code could be run:

require([get!object1], function(object1){})

Later in code, if object1 is required again, becasue and AMD module has been defined, the get! is no longer needed. eg:

require([object1], function(object1){})

clone

For objects that do not have a prototype, the base is whatever is returned by the AMD loaded. If you want to clone the base before injection, set clone: true.

For example:

'my/module': {
    directives: {
        clone: false
    }
}

Arrays of values and objects can be injected, and elements from those arrays can be defined in different parts of the config.

This config will result in a my/zoo instance with an array of six different animals:

'my/zoo': {
    params: {
        animals: [
            'cobra',
            'crocodile'
        ]
    },
    gets: {
        animals: [
            'lion1',
            'lion2'
        ]
    },
    proxies: {
        animals: [
            'tiger',
            {
                base: 'penguin',
                name: 'Percy'
            }
        ]
    }
}

It's all great and wonderful to inject objects here and there, but what if you have an object that only might be used? You don't want to go through the overhead of creating, particularly if it requires extra code to be loaded from the server if it is only rarely used. It's time to use a Proxy.

A proxy object is a stand in for the real object. Di can create a proxy, and the real object will only be created when the proxy is first used. Proxy object will be injected with their own configured params and proxies when the proxy is created. However, they will not be injected with any configured gets until first method call on the proxy.

When creating a proxy, the di needs to be told what methods are available for proxying. This is what the proxyMethods configuration is used for.

For example, with this config di.get('my/zoo') will return an instance of my/zoo populated with a proxy lion. The my/lion/module will not be created until zoo.lion.roar() is called.

'my/zoo': {
    proxy: {
       lion: 'my/lion/module'
    }
}
'my/lion/module': {
    proxyMethods: [
        'roar'
    ]
}

Note: Proxied methods may return a Deferred which will resolve to the proxied method's result. This is because fetching the insance through the di may involve asyncronous loading.

Every proxy also has a diGet() function that can force the di to load the proxy's underlying instance.

There are two AMD plugins provided which mean you can largely forget di in your actual code.

get!

Using havok/get!my/module will return an instance of my/module retrieved through the shared di container. Eg:

When creating a proxy, the di needs to be told what methods are available for proxying. This is what the proxyMethods configuration is used for.

For example, with this config di.get('my/zoo') will return an instance of my/zoo populated with a proxy lion. The my/lion/module will not be created until zoo.lion.roar() is called.

require(['havok/get!my/module'], function(myModule){
    //do something
})

proxy!

havok/proxy! works exactly the same as havok/get!, it just returns a proxy instead:

require(['havok/proxy!my/module'], function(myModule){
    //do something
})

When doing dojo builds, it is strongly recommended that you use the havok build tools. If you do, di plugins will resolve at build time with the following behaviour:

havok/get!

Will include havok/di/di, and the module requested by get!, and any modules in the gets property of the config for that object.

havok/proxy!

Will include havok/di/di, but not the module requested by proxy!.

havok/di/sharedDi!

Will include havok/di/di.

Loading more havok...