Dependency Injection Explained in JavaScript
I’m probably telling you what you already know, and if you don’t, it’s not too late to find out…
Why Dependency Injection
JavaScript frowns at global variables.
When working with individual scripts in the browser, it’s easy to attach an object created from one script, to the window
object and use it in another script. e.g.
<script>
var foo = 'bar'
</script>
<script>
console.log(foo) // should output 'bar'
</script>
In Node however, you’re working with modules which usually have no idea of the existence of others (or shouldn’t), and you have a lot of scripts which use code from other scripts, creating dependencies. e.g. in create-person.js
,
import db from 'my-db-framework'
export const createPerson = function (person) {
db.insert('tblPerson', person)
}
This code depends on the export ofmy-db-framework
, which is cool, until you have to test, or/and db
needs to be instantiated in a different way, and you have to edit multiple scripts that depend on db
to get that working.
How Dependency Injection Helps
Dependency Injection takes care of that by passing an already created instance of db
into the createPerson
function, like:
export const createPerson = function (db, person) {
db.insert('tblPerson', person)
}
So, createPerson
depends on an instance of db
, which we can instantiate in any form we like, which is cool for tests (because mocks), but not cool when writing code.
Factory Functions
I mean, do we have to pass an instance of db
every time we want to create a new person?
Couldn’t we pass it once and not have to pass it anymore?
Here come factory functions to save the day:
export const GetCreatePerson = function (db) {
return function (person) {
db.insert('tblPerson', person)
}
}
So now, when we require('./create-person')
, we get a function we can pass db
into, that returns the createPerson
function which already has db
in its closure.
const createPerson = require('./create-person')(db)
createPerson(new Person({ name: 'Mykeels' }))
Note: Factory Functions are given their name because they create stuff
Cool, yea?
What to look out for
When passing dependencies, you should probably ensure that the correct dependencies are passed into your factory functions, by checking them. e.g.
export const GetCreatePerson = function (db) {
if (!db) throw new Error('db is not defined')
else if (typeof(db.insert) !== 'function') {
throw new Error('invalid instance')
}
return function (person) {
db.insert('tblPerson', person)
}
}
This way, you’ll always ensure that no one makes the mistake of passing a wrong, or improperly-created dependency.
Multiple Dependencies
If you have multiple dependencies to deal with, you can use multiple parameters, or use a single object to encapsulate them. I like to use props
like:
export const GetCreatePerson = function (props) {
if (!props) throw new Error('props is not defined')
else if (!props.db) throw new Error('db is not defined')
else if (typeof(props.db.insert) !== 'function') {
throw new Error('invalid instance')
}
return function (person) {
props.db.insert(props.tables.tblPerson, person)
}
}
What we’ve learned
This is a really good way to pass dependencies into scripts, and makes it possible to test scripts as unit, by simulating its dependencies via a process called mocking.
Cheers!