Conditional Dependency Injection with AngularJS
The excitement level around AngularJS got high enough that I thought I should give it a shot, so I took it for a spin in a PhoneGap application I am working on.
My application defines an OAuth service that handles authentication and authorization. I want to inject different implementations of the OAuth service depending on the environment the application is running in. In a web browser the OAuth service simply wraps the Google API JavaScript client. In the PhoneGap web view the OAuth service uses a custom implementation that relies on InAppBrowser.
I think conditional dependency injection will be a common need for most PhoneGap developers, especially developers that hope to reuse some, or all, of their code in a web application.
Unfortunately, it is not immediately obvious how to do this…
Here was my first attempt:
var app = angular.module('app', []);
if (window.cordova) {
app.service('oauth', function(phonegapDependencies) {
this.authorize = function() {};
});
} else {
app.service('oauth', function(webDependencies) {
this.authorize = function() {};
});
}
app.controller('LoginController', function($window, oauth) {
oauth.authorize();
});
This is simple, but the problem with this approach becomes apparent when you need to make a decision about which service to use after bootstrapping. It also hurts testability. You can’t test both implementations in the same test run.
My next attempt was to define two services, declare a dependency on both, and choose which to use at runtime:
var app = angular.module('app', []);
app.service('webOauthImpl', function() {
this.authorize = function() {};
});
app.service('phonegapOauthImpl', function() {
this.authorize = function() {};
});
app.controller('LoginController', function($window, webOauthImpl, phonegapOauthImpl) {
if ($window.cordova) {
phonegapOauthImpl.authorize();
} else {
webOauthImpl.authorize();
}
});
This is somewhat better from a testability standpoint, but I still don’t like the thought of having these checks littered throughout my code. I want my controller to only be aware of one OAuth service. This will make it easier to use some other useful features of AngularJS, like decorators for example.
After digging around in the AngularJS documentation some more, I discovered providers, which allow more control over the dependency injection process. Here was my attempt at using providers:
var app = angular.module('app', []);
app.provider('oauth', function($window, phonegapDependencies, webDependencies) {
function webOauthImpl(webDependencies) {
this.authorize = function() {};
};
function phonegapOauthImpl(phonegapDependencies) {
this.authorize = function() {};
};
this.$get = function() {
if ($window.cordova) {
return new phonegapOauthImpl(phonegapDependencies);
} else {
return new webOauthImpl(webDependencies);
}
};
});
app.controller('LoginController', function(oauth) {
oauth.authorize();
});
Okay, now we’re getting somewhere. This is easy to test, I managed to keep the conditional in one place, and the controller only depends on a single OAuth service. The only thing I still don’t like is having to specify the dependencies for both implementations in the provider, and then pass the dependencies to the appropriate controller. I want AngularJS to do this for me…
Well, it can! I realized I could use the $injector
service with a factory
to accomplish exactly what I wanted:
var app = angular.module('app', []);
app.service('webOauthImpl', function(webDependencies) {
this.authorize = function() {};
});
app.service('phonegapOauthImpl', function(phonegapDependencies) {
this.authorize = function() {};
});
app.factory('oauth', function($window, $injector) {
if ($window.cordova) {
return $injector.get('phonegapOauthImpl');
} else {
return $injector.get('webOauthImpl');
}
});
app.controller('LoginController', function(oauth) {
oauth.authorize();
});
That’s it! First I register two different OAuth services, each with the same interface. Then I register a factory method that depends on the $window
and $injector
services. In the factory method I check whether or not the cordova
global variable is defined and resolve the appropriate OAuth implementation using the $injector.get
method. Now I can test each implementation in isolation and test that the OAuth factory gives the right implementation under the right conditions.