By:Gediminas Backevicius Posted On: Topic:Engineering

Dependency Injection in JavaScript

Everyone who codes with .NET, Java or PHP has heard about dependency injection (DI), inverse of control (IoC) or is using a special framework which implements the IoC/DI pattern. IoC and DI can be considered synonymous. Lately, applications written in JavaScript may consist of thousands of code lines, raising the question “Can a dependency injection pattern be used in JavaScript”?

Before answering this, let’s figure out what is dependency injection and what are advantages and disadvantages of it.

Dependency injection is a software design pattern that allows someone to remove hard-coded dependencies and makes it possible to change them. Dependencies can be injected to the object via the constructor or via defined method or a setter property.

Advantages:

  • Decreases coupling between an object and its dependency
  • Doesn’t require any change in code behavior, it can be applied to an existing code
  • Helps isolate the client from the impact of design changes and defects
  • Allows the system to be reconfigured without changing the existing code
  • Allows concurrent or independent development
  • Allows to make code more maintainable and testable, because dependencies’ impact can be removed by replacing dependencies with mocks or stubs

Disadvantages:

  • When instantiating a type you have to know which dependencies to use
  • Hides a type’s instantiation and dependency resolving logic and if an error occurs, it can be much harder to troubleshoot
  • It can require you to write more lines of codes
  • It can be slower when instantiating a type with keyword new, it’s related to meta data which has to be used while resolving instance

Let’s move from theory to practice with a simple example. In this example we have driver who owns a car and the car can have one engine.

Let’s write some code which implements the defined class diagram above.

    
     function Engine() {
    
       this.hp = 256;
    
     }
    
     Engine.prototype.start = function () {
    
       console.log("Engine with " + this.hp + " hp has been started...");
    
     }
    
     function Car() {
    
       this.name = "wv";
    
       this.engine = new Engine();
    
     }
    
     Car.prototype.start = function () {
    
       if (this.engine) {
    
         this.engine.start();
    
       }
    
     }
    
     function Driver() {
    
       this.name = "tom";
    
       this.car = new Car();
    
     }
    
     Driver.prototype.drive = function () {
    
       if (this.car) {
    
          this.car.start();
    
       }
    
     }
    
     var driver = Driver();
    
     driver.drive();
    
     // Engine with 256 hp has been started...

At first look everything appears fine. This code works if you don’t need to test or change it in the future. However, this code has some issues, one of which is that dependencies are hard-coded.

    
     function Engine(hp) {
       this.hp = hp;
    
     }
    
     Engine.prototype.start = function () {
    
       console.log("Engine with " + this.hp + " hp has been started...");
    
     }
    
     function Car(name, engine) {
    
       this.name = name;
    
       this.engine = engine;
    
     }
    
     Car.prototype.start = function () {
    
       if (this.engine) {
    
         this.engine.start();
    
       }
    
     }
    
     function Driver(name, car) {
    
       this.name = name;
    
       this.car = car;
    
     }
    
     Driver.prototype.drive = function () {
    
       if (this.car) {
    
          this.car.start();
    
       }
    
     }
    
     var driver = Driver("tom");
    
     driver.car = new Car("wv", new Engine(256)));
    
     driver.drive();
    
     // Engine with 256 hp has been started...

We have modified our existing code and it is extensible, but the driver’s creation procedure has become more complex. In this case, a dependency injection container can help us. You can define a driver’s details in configuration and when you want to instantiate the driver you have to do it using the dependency injection resolver. In the example below di4js library is used.

     di
    
       .register('engine')
    
         .as(Engine)
    
           .withConstructor()
    
             .param().val(256)
    
       .register('car')
    
         .as(Car)
    
           .withConstructor()
    
             .param().val('wv')
    
             .param().ref('engine')
    
       .register('driver')
    
         .as(Driver)
    
           .withConstructor()
    
             .param().val('tom')
    
             .param().ref('car');
    
     var driver = di.resolve('driver');
    
     driver.drive(); // Engine with 256 hp has been started…

As I mentioned before, dependency injection may require you to write more code, as evidenced in this example. However, there is one advantage. The configuration is defined in a single place and it can reconfigured without changing the existing code base.

Autowire

If you do not want to define all dependencies manually, you can enable the autowire option. If it is enabled for dependency resolver, all type or instance dependencies are resolved automatically by name. It may reduce the amount of code necessary to register types and their dependencies. With the autowire option enabled you only have to register types. By default autowire is disabled.

Let’s simplify the example in order to show how the autowire option works.

     function Engine() {
    
     }
    
     Engine.prototype.start = function () {
    
       console.log("Engine has been started...");
    
     }
    
     function Car() {
    
       this.engine = null;
    
     }
    
     Car.prototype.start = function () {
    
       if (this.engine) {
    
         this.engine.start();
    
       }
    
       console.log("Car has been started...");
    
     }
    
     di
    
       .autowired(true)
    
       .register('engine')
    
         .as(Engine)
    
       .register('car')
    
         .as(Car);
    
     var car = di.resolve('car');
    
     car.start();

We will see this output in console.

 "Engine has been started..."

 "Car has been started..."

The most common scenario is mixed mode. By default you can allow dependencies to automatically resolve, but some dependencies can be defined manually. Manually defined dependencies have a bigger priority.

If you are using a different naming convention from the one given in the example it can be overridden easily. For more details read di4js documentation.

List of Dependencies

In some situations you may need to resolve a list of dependencies. To achieve this you have to make multiple registrations for a single name.

Let’s modify our previous example and say that the driver can own more than one car at the same time. Modified class diagram can be found below.

First of all we need to modify the driver’s definition. The property ‘car’ has to be renamed to ‘cars’ and an additional parameter ‘name’ has to be added to the ‘drive’ method. Let’s look to our modified source code.

     function Driver(name) {
    
       this.name = name;
    
     }
    
     Driver.prototype.drive = function (name) {
    
       if (this.cars && name) {
    
         if (this.cars instanceof Array) {
    
           for (var i = 0; i < this.cars.length; i++) {
    
             if (this.cars[i].name === name) {
    
               this.cars[i].start();
    
               break;
    
             }
    
           }
    
         } else if (this.cars.name === name) {
    
           this.cars.start();
    
         }
    
       }
    
     }

Now it’s time to register driver’s dependencies and try to resolve it.

     di
    
       .register('engine')
    
         .as(Engine)
    
           .withConstructor()
    
             .param().val(256)
    
       .register('car')
    
         .as(Car)
    
           .withConstructor()
    
             .param().val('wv')
    
             .param().ref('engine')
    
       .register('car')
    
         .as(Car)
    
           .withConstructor()
    
             .param().val('ford')
    
             .param().ref('engine')
    
       .register('tom')
    
         .as(Driver)
    
           .withConstructor()
    
             .param().val('tom')
    
           .withProperties()
    
             .prop("cars").ref('car');
    

To instantiate the driver by name we need to write a single code line.

   
     var tom = di.resolve('tom');
    

If we invoke method ‘drive’ we will get a result which will be printed to a console output.

     tom.drive('wv');
    
     // Engine with 256 hp has been started…
    
     // Car 'wv' has been started...

Using Setter methods Instead of Properties/Fluent Interface

There is one more fancy feature. Some objects do not have properties, they have only setter methods and fluent interface. Such scenario with di4js library can be handled too. There is one limitation that these dependencies can not be resolved automatically.

Let’s modify our previous example and replace properties with setter methods.

New defined methods look like this.

     Car.prototype.setEngine = function (engine) {
    
       this.engine = engine;
    
       return this;
    
     };
    
     Driver.prototype.setCars = function (cars) {
    
       this.cars = cars;
    
       return this;
    
     };

Below you can see how a modified type and its dependencies registration look.

di

 .register('engine')

   .as(Engine)

     .withConstructor()

       .param().val(256)

 .register('car')

   .as(Car)

     .withConstructor()

       .param().val('wv')

     .withProperties()

       .func('setEngine')

         .param().ref('engine')

 .register('car')

   .as(Car)

     .withConstructor()

       .param().val('ford')

     .withProperties()

       .func('setEngine')

         .param().ref('engine')

 .register('tom')

   .as(Driver)

     .withConstructor()

       .param().val('tom')

     .withProperties()

       .func('setCars')

         .param().ref('car');

If we resolve the driver and invoke its method ‘drive’, we will get the same result as in the previous examples.

Child Container

In all of these examples, the global dependency resolver is used because it’s good for demo purposes and small apps. For bigger apps, the child dependency resolvers should be created per scope. It allows you to have better flexibility and extensibility. A parent can be extended or overridden by a child without making an impact on it.

First of all, lets register the type in the global dependency resolver and then try to resolve the registered type by name.

 var DieselEngine = function () {

 };

 DieselEngine.prototype.start = function () {

   console.log("Diesel engine has been started...");

 };

 function Car(engine) {

   this.engine = engine;

 }

 Car.prototype.start = function () {

   if (this.engine) {

     this.engine.start();

   }

   console.log("Car has been started...");

 }

 di

   .autowired(true)

   .register('engine')

     .as(DieselEngine)

   .register('car')

     .as(Car);

 var car = di.resolve('car');

 car.start();

We are getting such results.

 "Diesel engine has been started..."

 "Car has been started..."

Now we can create a child container from the parent and try to resolve the overridden type.

     var PetrolEngine = function () {
    
     };
    
     PetrolEngine.prototype.start = function () {
    
       console.log("Petrol engine has been started...");
    
     };
    
     var child = di
    
       .create()
    
       .register('engine')
    
         .as(PetrolEngine);
    
     car = child.resolve('car');
    
     car.start();
    
    Now we are getting different results.
    
     "Petrol engine has been started..."
    
     "Car has been started..."

Injection to Function or Object

There is a situation when instance creation can’t be controlled by the dependency injection resolver. This situation may occur when you are adapting the dependency injection for a legacy code or instances are created by other components which you do not control. There is an option to inject dependencies to object or to function.

The first example demonstrates how dependencies can be injected into the function using parameter names.

     di.inject(function (car) {
    
       car.start();
    
     });
    
    In the second example, an array notation is used to inject dependencies into the function.
    
     di.inject(['car', function (car) {
    
       car.start();
    
     }]);

In the previous examples I have demonstrated how to inject dependencies into the function. In the following examples I am going to demonstrate how dependencies can be injected into an object.

    
     var car = new Car();
    
     di.inject(car);
    
     car.start();

There is the second method for injecting dependencies into object. You have to provide a registration name which has to be used while injecting dependencies.


     var car = new Car();
    
     di.inject(car, 'car');
    
     car.start();

AMD

di4js is compatible with asynchronous module definition (AMD) and it can be loaded as an ordinal module.

     define(['di4js'], function (di) {
    
       function Engine(hp) {
    
         this.hp = hp;
    
       }
    
       Engine.prototype.start = function () {
    
         console.log("Engine with " + this.hp + " hp has been started...");
    
       }  
    
       di
    
         .register("engine")
    
           .as(Engine)
    
             .withConstructor()
    
               .param().val(256);   
    
       var engine = di.resolve("engine")
    
       engine.start();
    
     });

Testing

As I mentioned before, if you are using the dependency injection, your code is maintainable and testable because dependencies can be easily replaced with stubs or mocks.

In the following example I am going to use a well-known testing framework, Jasmine, to demonstrate how we can test a single method, replacing dependencies with mocks.


     describe("car", function() {
    
       it("should start a car", function() {
    
         var engineMock = jasmine.createSpyObj('engine', ['start']);
    
         var car = new Car(engineMock);
    
         car.start();
    
              expect(engineMock.start).toHaveBeenCalled();
    
       });
    
     });

As you can see in the example, the code can be tested easily.

Why di4js?

di4js is a lightweight library which is inspired by Unity, Spring, AngularJS and others. di4js’s advantages are that it is a small library and it’s dedicated to solving a certain problem, thus it’s not a massive framework which tries to cover everything. It also doesn’t depend on other libraries, so it’s easy to embed into an existing code base.

You may ask why it was created and the answer is simple. A range of libraries was investigated and none of them met expectations. So, the goal was to gather various components from various libraries into one single place. Initially it was written for internal purposes but it’s shared.

di4js can be used in projects which are not built on large frameworks, such as AngularJS, because large frameworks have built-in solutions for dependency management. di4js can be useful when you rely on small dedicated libraries.

Usage

di4js library is released under the MIT license. It can be used in modern browsers or in other JavaScript runtimes such as Node.js. The library can be installed from various package managers: nuget, npm or Bower.

To install di4js module for Node.js, this command should be used:

 npm install di4js

To install di4js for a web browser, run the following command:

 bower install di4js

In Visual Studio, the di4js module can be installed using NuGet extension. To install di4js run the following command in the package manager console.

 Install-Package di4js

If you want to look at the source code or to get more information about library or its usage, just vist https://github.com/gedbac/di4js.

Final Thoughts

At the beginning we asked a question “Can a dependency injection pattern be used in JavaScript?” Now we can say that a dependency injection can be easily used and is useful in JavaScript, but the project size and complexity always has to be considered. Thus, the problem has to be identified and after that, a corresponding solution has to be selected. Consequently, we can say that every pattern which is used in other languages can be easily adapted to JavaScript.

 

Gediminas Backevicius

Want more industry news?

comments powered by Disqus
Let's Talk