Dependency Injection Containers in JavaScript

Dependency Injection is a really important design pattern that will aid in producing clean, loosely coupled and easy to test code. The benefits of dependency injection could go into its own blog post, but for this one, I want to focus on dependency injection containers in JavaScript.

Below is a code snippet, showing a function that creates a new instance of a service, versus a function that has the dependency injected into it as a parameter. Look how much cleaner the second function is!

const exampleFunction = () => {
  const service = new Service()
  return service.serviceMethod()
}
const exampleFunctionWithDI = (service) => {
  return service.serviceMethod()
}

Why use a Dependency Injection Container

A dependency injection container allows you to configure an application’s components and what each component depends on. The container will then be able to inject the dependency into the component for you.

The container can be a powerful tool and is utilised in many languages and frameworks, for example, Spring’s IoC container allows “Beans” to be injected, either in the constructor or field of the class that requires it.

A container means you don’t have to manually wire in the dependencies and have a flood of the ‘new’ keyword in your code base.

BottleJS


BottleJS is a JavaScipt dependency injection container that is lightweight and inspired by the AngularJS Module API. I have had the most success using this container service when creating a NodeJS application rather than a front-end project.

You first create a bottle which will hold all the application components:

# app.js
const Bottle = require('bottlejs')
const bottle = new Bottle()
Then define services and attach it to the bottle.
# src/genericService.js
class genericService {
  getData () {
    return 'data'
  }
}

module.exports = bottle => {
  bottle.service('genericService', genericService)
}

Hang on! Why are we exporting a function with a bottle as an argument and then defining the service? This is so that we can bootstrap the whole application using reflection – we will see this later on.

The call to bottle.service accepts the name of the service as the first argument, then the service itself as the next argument.

We now define a controller that depends on this service.

# src/genericController.js
class genericController {
  constructor(genericService) {
    this.genericService=genericService
  }

  getData() {
    return this.genericService.genericService()
  }
}

module.exports = bottle => {
  bottle.service('genericController', genericController, 'genericService')
}

The above example looks similar to the genericService example, but in this snippet, the bottle.service call has a third argument. For bottle.service, all arguments given after the second argument are string values to the dependencies you want injecting. So in this example, the genericController will have the genericService defined above injected into the constructor of genericController.

To bootstrap the whole application, you can use glob to traverse the source files, require them and call the exported function with the bottle container.

# app.js
const glob = require('glob')
// traverse the file system, passing this bottle to every javascript file we find
glob.sync(path.join(path.resolve(__dirname), '/src/**/*.js')).forEach((match) => {
  require(match)(bottle)
});

When app.js is now run, the genericController now has genericService injected into it.

This application can be expanded with more components following the same pattern as outlined above.

Testing

Of course, what’s the point of a dependency injection container, if it makes testing a pain. Thankfully, with bottlejs, it was developed with testing in mind. The component you are testing can have the dependencies mocked very easily, see the code example below:

const Bottle = require('bottlejs')
const genericController = require('./genericController.js')

describe('genericController test', () => {
  beforeEach(() => {
    this.bottle = newBottle()
    this.bottle.service('genericService', () => {
      this.getData = () => {
        return 'mock-data'
      }
    })
  })

  it('should get data', () => {
    genericController(this.bottle)
    const expectedResult = 'mock-data'
    const actualResult = this.bottle.container.genericController.getData()
    expect(actualResult).to.equal(expectedResult)
  })
})

The beforeEach hook sets up a mock genericService that returns mock data. That service is then injected into the bottle container, and then that service is injected into the genericController to test. A bottle’s services can be accessed via the bottle.container object, as shown in the snippet.

Leave a Comment