~/ Dependency Injection 101

"Dependency injection" is a somewhat scary term to explain loading modules and making sure memory footprint is minimal.

I've been thinking a lot about Angular's $injector and how I might build a dependency injector from scratch.

A thing that creates things

À la angular, I wanted an injector as the global namespace, a module method that allows me to both read and create modules. Let's start with creating a module.

We want the interface to be something like

injector.module("egg",[],{
hatch: function(){
console.log("chirp chirp")
}
})

Where the name of the module is the first argument, the second argument is an array of dependencies, and the third argument is the module's prototype (methods and attributes).

var injector = { // global namespace
module: function(name, dependencies, prototype){
var module = { // a custom template for modules
name: name,
dependencies: dependencies
}
for( var attr in prototype){
module[attr] = prototype[attr] // attach proto methods to module
}
this._modules[name] = module
return module
},
_modules: {}
}
var egg = injector.module('egg',[],{
hatch: function(){
console.log("chirp chirp")
}
})
egg.hatch()

If there is only the first argument, the injector assumes the module has already been defined and returns the module's prototype.

var injector = { // global namespace
module: function(name, dependencies, prototype){
if(!dependencies){
return this._modules[name]
}
var module = { // a custom template for modules
name: name,
dependencies: dependencies
}
for( var attr in prototype){
module[attr] = prototype[attr] // attach proto methods to module
}
this._modules[name] = module
return module
},
_modules: {}
}
injector.module("egg").hatch()

Now that we can read and write modules, let's try seeing if a module's dependencies have been loaded before calling methods.

The angular-style of injecting dependencies is:

injector.module('name of module',['array','of','dependencies',function(array, of, dependencies){
// if each module name in the array is defined and the last element
// is a function, invoke the function with each dependency as an argument.
}])

Raising Errors when Dependencies Are Not Met

If the specified dependency is not already defined in injector._modules, raise an error:

for( var i = 0; i < dependencies.length; i++ ){
var dependency = dependencies[i]
var loaded = this._modules[dependency] ? true : false
if(!loaded){
throw new Error("Dependency not met: " + dependency)
}
}

https://repl.it/BJrx/1

Passing Modules to functions

While in the loop, if the typeof the element is a function, we can assume it is the last element of the array, and invoke it with each of the modules.

if( typeof dependency == "function" ){
var modules = dependencies.map(function(d){ // map names of modules to actual modules
return this._modules[d]
}.bind(this)) // preserve context
return dependency.apply(this, modules) // unknown number of dependencies
}

This code must come before the error-raising code above. That is, a return is necessary after the function's invocation to prevent the "module not met" dependency error.

We now have an interface like this:

injector.module('chicken',['egg', function(egg){
egg.hatch()
}])

https://repl.it/BJrx/2

Next Steps

Now that we have a way to create, read, and depend on modules, where do we go from here?

I was thinking it would be nice to be able to asynchronously load modules, but I'll leave that for another day.

Here's the dependency injector all together:

var injector = { // global namespace
module: function(name, dependencies, prototype){
if(!dependencies){
return this._modules[name]
}
for( var i = 0; i < dependencies.length; i++ ){
var dependency = dependencies[i]
if( typeof dependency == "function" ){
var modules = dependencies.map(function(d){ // map names of modules to actual modules
return this._modules[d]
}.bind(this)) // preserve context
return dependency.apply(this, modules) // unknown number of dependencies
}
var loaded = this._modules[dependency] ? true : false
if(!loaded){
throw new Error("Dependency not met: " + dependency)
}
}
var module = { // a custom template for modules
name: name,
dependencies: dependencies
}
for( var attr in prototype){
module[attr] = prototype[attr] // attach proto methods to module
}
this._modules[name] = module
return module
},
_modules: {}
}
injector.module('egg',[],{
hatch: function(){
console.log("chirp chirp")
}
})
injector.module('chicken',['egg', function(egg){
egg.hatch()
}])

https://repl.it/BJrx/4 | https://github.com/jshawl/dependency-injection


~/ Posted by Jesse Shawl on 2015-09-20