Dependency Injection: Angular vs. RequireJS
If you have built large JavaScript applications before, chances are, you have faced the task of managing component dependencies. You can think of a component as a block of functionality. It may be a function, object, or an instance. The block chooses to expose one or more public methods. It may also choose to hide non-public functionality. In this article, we will look at two major libraries, AngularJS and RequireJS. We will analyse how they use dependency injection to share components across an application.
Short Story on Dependency Injection
Dependency injection becomes a necessity when you need an easy way to pull in one or more components into an application. For example, assume you have two components named database
and logger
. Assuming that the database
component exposes the methods getAll
, findById
, create
, update
, and delete
. The logger
component only has one method, saveNewLog
, in it’s public API. Let’s assume the logger
component depends on the database
component to function. Using dependency injection, we could pass in the database
component as a dependency to the logger
component during creation.
Just so you can visualize the dependencies better, I’ll write it in code. Note that the actual syntax depends on the dependency injection library you use. Angular and RequireJS have different syntax, so the code below is a generic example and we’ll get to actual representations of the two libraries in a bit.
Here is the database
API:
function database() {
var publicApis = {
getAll: function() {},
findById: function(id) {},
create: function(newObject) {},
update: function(id, objectProperties) {},
delete: function(id) {}
};
return publicApis;
}
And, here is the logger
API:
function logger(database) {
var publicApis = {
saveNewLog: function() {}
};
return publicApis;
}
As you can see, we are passing the database
component into the constructor of the logger
. The part of the application which handles the instantiation of the logger
must provide it with an instance of a database
component.
The Need for Dependency Injection
Now that we are more educated about what dependency injection is, let’s identify what benefits it brings to the table. If you are an advocate of good JavaScript design, some benefits of dependency injection may be obvious to you. If they’re not, let me explain a few of the general benefits. I believe these apply across the board whether you use AngularJS or RequireJS.
Testing Becomes a Breeze
Testing becomes much easier because you can provide mocked dependencies instead of real implementations.
Separation of Concerns
Dependency injection lets you separate the parts of your application so that each one handles a distinct job. In the above example, the database
module is only concerned with dealing with a database. The logger
module is only responsible for logging data, whether it is in a database, file, or the console. The benefit of this is easier swapping of dependencies. If we later decide that we need to use a file-based database instead of a traditional relational database, we just have to pass in a different module. This module just has to exposes the same API methods as the database
module, and the logger
module would continue to work properly.
Easier Reusability of Components
Due to this nature of separating concerns, we can reuse components. This makes it easy to reuse external libraries which also follow the same pattern.
Dependency Management Libraries
We’ve seen some of the benefits, now let’s compare two major libraries in the game – Angular and RequireJS. RequireJS is dedicated to dependency management. AngularJS provides much more than dependency management, but we’ll only focus on that capability.
AngularJS
AngularJS has these things called recipes. A recipe is analogous to a component which was described earlier on. Examples of Angular components are factories, directives, and filters. Angular provides several ways to inject a component into something else. We will use the database
and logger
components as an example.
Before we dive into the different ways to do dependency injection with Angular, let’s build our example scenario first. Assuming we have an Angular module named myModule
, let’s create a UserController
:
function UserController() {
//some controller logic here
}
We also have database
and logger
services defined:
myModule.factory('database', function() {
var publicApis = {
getAll: function() {},
findById: function(id) {},
create: function(newObject) {},
update: function(id, objectProperties) {},
delete: function(id) {}
};
return publicApis;
});
myModule.factory('logger', function(){
var publicApis = {
saveNewLog: function() {}
};
return publicApis;
});
Let’s assume the UserController
depends on the logger
component to function. Of course, the logger
component still depends on the database
component. We can represent the dependencies in AngularJS in three different ways.
Parameter Name Inferring
This method depends on the names of function parameters when reading in dependencies. We can apply it to the example above like this:
function UserController(logger) {
//some controller logic here to use injected logger factory
}
myModule.factory('database', function() {
var publicApis = {
getAll: function() {},
findById: function(id) {},
create: function(newObject) {},
update: function(id, objectProperties) {},
delete: function(id) {}
};
return publicApis;
});
myModule.factory('logger', function(database) {
//use injected database factory here
var publicApis = {
saveNewLog: function() {}
};
return publicApis;
});
Using $inject
This dependency injection method uses $inject
property on the function of your component. The $inject
property should be an array of strings specifying the dependencies. For the UserController
, this is easy to do. For the logger
factory we’ll need to change the example above a little bit so we can add the property to its function. Since it’s an anonymous function, we should first define it as a named function. Next, we can attach the required property, as shown below.
function UserController(logger) {
//some controller logic here to use injected logger factory
}
UserController['$inject'] = ['logger'];
myModule.factory('database', function() {
var publicApis = {
getAll: function() {},
findById: function(id) {},
create: function(newObject) {},
update: function(id, objectProperties) {},
delete: function(id) {}
};
return publicApis;
});
function loggerFactory(database) {
//use injected database factory here
var publicApis = {
saveNewLog: function() {}
};
return publicApis;
}
loggerFactory['$inject'] = ['database'];
myModule.factory('logger', loggerFactory);
Using Array Notation
The third way involves passing in an array as the second parameter when defining the UserController
and the logger
factory. Here, we also have to change the way we define the UserController
so we can use this method.
function UserController(loggerFactory) {
//some controller logic here to use injected logger factory
}
myModule.controller('UserController', ['logger', UserController]);
myModule.factory('database', function() {
var publicApis = {
getAll: function() {},
findById: function(id) {},
create: function(newObject) {},
update: function(id, objectProperties) {},
delete: function(id) {}
};
return publicApis;
});
function loggerFactory(database) {
//use injected database factory here
var publicApis = {
saveNewLog: function() {}
};
return publicApis;
}
myModule.factory('logger', ['database', loggerFactory]);
RequireJS
Dependency Injection with RequireJS works by having components in files. Each component lives in its own separate file. Whereas AngularJS loads the components upfront, RequireJS only loads a component when needed. It does this by making an Ajax call to the server to get the file where the component lives.
Let’s see how RequireJS handles dependency injection syntactically. I will skip over how to setup RequireJS. For that, please refer to this SitePoint article.
The two main functions concerned with RequireJS dependency injection are define
and require
. In short, the define
function creates a component, while the require
function is used to load a set of dependencies before executing a block of code. Let’s inspect these two functions in a bit more depth.
The define
Function
Sticking with the logger
and database
example, let’s create them as components (the filename:
comments indicate where we would actually define the components):
//filename: database.js
define([], function() {
var publicApis = {
getAll: function() {},
findById: function(id) {},
create: function(newObject) {},
update: function(id, objectProperties) {},
delete: function(id) {}
};
return publicApis;
});
//filename: logger.js
define(['database'], function(database) {
//use database component here somewhere
var publicApis = {
saveNewLog: function(logInformation) {}
};
return publicApis;
});
As you can see, the define
function takes two parameters. The first one is an optional array of components which must be loaded before the component can be defined. The second parameter is a function which must return something. You may notice that we are passing in the database
component as a dependency for defining the logger
module. The database
component does not rely on any other component. Hence, its define
function takes an empty array as the first argument.
The require
Function
Now, let’s look at a scenario where we make use of the defined components. Let’s simulate logging some information. Since we need the logger
component to be able to make use of its code, we must pull it in using the require
function.
require(['logger'], function(logger) {
//some code here
logger.saveNewLog('log information');
});
As you can see above, the require
function is only used to run some code and does not return anything. The first parameter it accepts is an array of dependent modules. The second is the function to run when those dependencies have been loaded. This function accepts as many parameters as there are dependencies to load. Each one represents the corresponding component.
Conclusion
This brings us to the end of this comparison between AngularJS and RequireJS when it comes to dependency injection. Although the two take fairly different approaches, there’s no reason why they cannot work together. Please let us know what your experience is using these two libraries or if you have anything else to add.