Overview
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.
Config Format
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. |
base
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' } }
params
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} } }
gets
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' } } }
Proxies
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.
directives
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 } }
Injecting Arrays
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' } ] } }
Proxy Objects
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.
Plugins
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 })
Build
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
.