Dojo FAQ: Does Dojo have dependency injection like Angular

By on June 25, 2014 10:19 am

DojoFAQ

Dependency injection allows for loose coupling in an application’s architecture, which can simplify testing, and make it easier to compose application components in novel ways. AngularJS ships with a dependency injection system out of the box, but the same benefits can be realized in Dojo.

In order to understand how dependency injection might work in Dojo, we will first examine the AngularJS dependency injection system.

To achieve some of the flexibility that AngularJS’s dependency injection system provides in pure JavaScript, you could follow a pattern like this:


function Controller(view, store) {
  this.view = view;
  this.store = store;
}

var view = new View();
var store = new Store();
var controller = new Controller(view, store);

In the above code example, we are passing in the instances of store and view to the controller constructor, which should not instantiate its own store and view. Injecting our dependencies allows us to reuse the controller with any store or view that might conform to the same API expected by the controller. This pattern makes it easier to unit test our controller. We can pass in mock views and stores and make assertions against the behavior of the controller. At its most basic level, AngularJS’s dependency injection is doing the same thing, but with a form of dependency lookup based on the function signature.

AngularJS has the notion of a module, but not the same meaning as an AMD or CommonJS module. An AngularJS module is the configuration for the dependency injector. The module configures the mappings the injector uses, so that when you ask for an object you get the one that makes sense for that module.

The AngularJS dependency injection system allows you to define what objects you depend on, and it then passes those objects to you based on the current module configuration. You can ask for built-in Angular factories, services, and providers. You also have the ability to register your own factories, services, and providers on the module.

// create our Angular module
var module = angular.module('app', []);

// configure the injector with our data store
module.value('dataStore', new Store());

// Angular will inject our dependencies, $scope, $document, 
// dataStore, when the controller is invoked
module.controller('appCtrl', function($scope, $document, dataStore) {

  $scope.title = $document[0].title;
  dataStore.set('title', $scope.title);

});

When testing AngularJS applications, it’s easy to pass in mocks or stubs for dependencies. The injector gives you the flexibility to redefine what an object is and then give it to your function when it’s time for the function to run.

In Dojo there are two ways in which you can achieve the same result, and depending on your needs you could choose one or the other or some combination of both.

To achieve dependency injection in Dojo, make sure that dependencies are passed into your constructor when the object is instantiated. Avoid creating instances within the constructor of another object. Even though there is no built in mechanism for automatically injecting the dependencies, it can be done rather easily with little code.

This widget would be difficult to test, because there is no way to easily swap out the store:

define([
  'dojo/_base/declare',
  'dijit/_WidgetBase',
  'app/store/Store'
], function (declare, _WidgetBase, Store) {
  return declare(_WidgetBase, {
    url: 'http://path/to/server',
    postMixInProperties: function () {
      this.inherited(arguments);
      // create store based on the url param
      // This is NOT the right way to do this
      this.store = new Store({ url: this.url });
    }
  });
});

With a little bit of planning we can design the widget to take the store as an option. This allows us to easily inject any store we need later on:

define([
  'dojo/_base/declare',
  'dijit/_WidgetBase'
], function (declare, _WidgetBase) {
  return declare(_WidgetBase, {
    store: null
  });
});

When working with existing code, it can be difficult to refactor it to allow for dependency injection. When trying to make existing code easier to test you can use require.undef to change which module is loaded. This is useful for unit testing since it allows you to provide mocks to isolate the module under test.

For example if we have a controller:

define([
  'app/widgets/list'
], function (List) {
  return function AppController() {
    this.listWidget = new List();
  };
});

Using require.undef we can mock the list for easier testing:

require.undef('app/widget/list');
// Note that this example follows Angular naming conventions
// A dojo example would typically use app/widgets/List
require({ map: {
  'app': { 'app/widgets/list': 'app/tests/mocks/list' }
} });
require([ 
  'app/controllers/AppController' 
], function (AppController) {
  //....
});

By thoughtfully designing our objects to accept dependencies when they are created, we are moving the choice of the concrete implementation outside of those objects. This allows us to inject any implementation we might need, for testing or other purposes. AngularJS has decided to push the idea of dependency injection into the framework itself, but getting the same results from a Dojo application is easy. You also have the advantage of better visibility into, and understanding of the dependency injection mechanism in your Dojo codebase, whereas in Angular the mechanism is opaque, so there could be some confusion around how the injector works.

Learning more

We also cover JavaScript application architecture, dependency injection, and writing testable code in our Dojo 202 workshop offered throughout the US, Canada, and Europe, or at your location. We also provide expert JavaScript and Dojo support and development services, to help you get the most from JavaScript, Dojo, and JavaScript application architecture. Contact us to discuss how we can help you define and implement the right architecture for your application.