Saturday, August 23, 2014

AngularJS Lifetime Management, Lazy-Loading, and other Advanced DI Techniques

One aspect of Angular that I love is it’s dependency injection. Contrary to some criticisms I’ve read, I find it is extremely flexible and powerful enough to address the demands of enterprise line of business apps. I discussed the general benefits of DI in Dependency Injection Explained and specifically Angular’s implementation using Providers, Services, and Factories and even aspect-oriented interception/decoration.

In this post, I address some other features common to advanced Inversion of Control containers, namely lazy-loading, lifetime management, and deferred creation/resolution.

Lazy-Loading

Lazy-loading simply refers to the late instantiation of objects when you need them. Many dependency injection systems will build-up a component the first time it is recognized as a dependency, but in some cases you may not want to instantiate the component until later in the application lifetime. In Angular, the perfect example is when you are setting up a behavior in the configuration pass that references components that haven’t been created yet.

Let’s assume you want to intercept the built-in $log service so that it stores entries on the $rootScope. I don’t recommend this but it works for a simple, contrived example. To intercept, you reference $provide in the configuration pass and then call the decorator method. If you try to reference the $rootScope directly, you’ll get an exception because of circular dependencies. The solution is to lazy-load the $rootScope instead, using the $injector.

The following code will only load the $rootScope the first time it’s needed.

$provide.decorator('$log', ['$delegate', '$injector',
    function ($delegate, $injector) {
        var log = $delegate.log.bind($delegate);
        $delegate.log = function (msg) {
            var rs = $injector.get('$rootScope');
            if (rs.logs === undefined) {
                rs.logs = [];
            }
            rs.logs.push(msg);
            log(msg);
        };
        return $delegate;
}]);

Subsequent calls will always get the same singleton instance of $rootScope. Here is the working fiddle. I’ve heard this (erroneous) criticism before (that Angular only supports singletons) … not true. The methods on the $injector are what you use to manage the lifetime of your components.

Lifetime Management

Lifetime management relates to how you handle instances of components. By default, when you inject an Angular dependency, the dependency injection will create a single copy and reuse that copy throughout your app. In most circumstances this is exactly the behavior you want. There are some solutions that may require multiple instances of the same component. Consider for a moment a counter service:

function Counter($log) {
    $log.log('Counter created.');
} angular.extend(Counter.prototype, {
    count: 0,
    increment: function () {
        this.count += 1;
        return this.count;
    }
}); Counter.$inject = ['$log']; app.service('counter', Counter);

Your app may need to keep track of different counters. When you inject the service, you always get the same counter. Is this an Angular limitation?

Not exactly. Again, using the $injector service you can instantiate a new copy any time you like. This code uses two separate counters:

app.run(['$rootScope', 'counter', '$injector',
    function (rs, c, i) {
        rs.count = c.count;
        rs.update = c.increment;
        rs.update2 = function () {
            var c = i.instantiate(Counter);
            rs.count2 = c.count;
            rs.update2 = function () {
                c.increment();
                rs.count2 = c.count;
            };
        };
    }]);

You can see each count is tracked in a separate instance in the working fiddle. If you know you are going to generate new instances often, you can register the service like this:

app.factory('counterFactory', ['$injector',
    function (i) {
        return {
            getCounter: function () {
                return i.instantiate(Counter);
            }
        };
    }]);

Then it’s simple to grab a new instance as needed, and you can reference your factory component instead of the $injector:

app.run(['$rootScope', 'counterFactory',
    function (rs, cf) {
        var c1 = cf.getCounter(),
            c2 = cf.getCounter();
        rs.count = c1.count;
        rs.update = c1.increment;
        rs.count2 = c2.count;
        rs.update2 = function () {
            rs.count2 = c2.increment();
        };
    }]);

You can check out this version by running the full fiddle here. As you can see, it is entirely possible to manage the lifetime of your components using Angular’s built-in dependency injection. But what about deferred resolution – i.e. those components you may introduce after Angular is already configured but still need to be wired up with their own dependencies?

Deferred Resolution

You’ve already seen one way you can defer the resolution of dependencies in Angular. When you want to wire something up you can call instantiate on the $injector service and it will resolve dependencies using either parameter sniffing, by looking for a static $inject property, or by inspecting an array you pass in. In other words, this is perfectly valid:

$injector.instantiate(['dependency', Constructor]);

You can also invoke a function decorated with an array as well. If you have a function that depends on the $log service, you can invoke it at run-time with the dependency resolved like this:

var myFunc = ['$log', function ($log) {
    $log.log('This dependency wired at runtime.');
}]; $injector.invoke(myFunc);

You can check out the working fiddle here (open your console to verify what happens when you click the button).

Summary

In summary, Angular’s dependency injection provides many advanced features you would expect and often require for a line of business application. The shortcut methods for factories, services, and providers sometimes confuse Angular developers into believing these are the only options available. The magic really happens on the $injector service where you can grab your singleton instance, build up a new component or dynamically invoke a function with dependencies.

As one final note, the injector is available to your client code even outside of Angular. To see an example of JavaScript code that is wired up outside of Angular yet uses the injector to grab the $log service, click here. Why is ‘ng’ passed in the array for the function? This is the core Angular module that is added implicitly when you wire up your own modules but must be explicitly included when you directly create your own instance of the injector.

7 comments:

  1. You could argue that I'm being pedantic but that is not dependency injection. That is service location. Non-varying service location is an anti-pattern (atleast in modern languages). Is it just that javascript it too aged to support actual constructor injection that it has to be beaten in with a hammer and done as service location?

    $injector really should be $locator

    ReplyDelete
  2. I'm not sure I agree entirely. $injector.get is service location but $injector.instantiate is dependency injection. I can annotate an object, call it, and have the dependencies injected. Constructor injection works fine with listing the parameters in the function constructor, the only reason you would go on to annotate with the array or static property is to preserve the dependency through minification.

    ReplyDelete
    Replies
    1. "$injector.instantiate is dependency injection" I guess i can't argue that (from looking at the code, I don't know what it actually does just infer) but that's basically dependency injection WITHOUT inversion of control. I'm not really sure what you're gaining to have DI without IOC.

      app.factory('counterFactory', ['$injector',
      getCounter: function () {
      return i.instantiate(Counter);

      is really no different than

      return new Counter(new Foo(), new Bar()).

      I guess there's some slight benefit if you add Baz as a dependency to Counter you don't need to modify existing usages or that you could leverage the life cycles determined by the container. Nevertheless I still don't see value being created.

      I'm used to my IOC containers providing value, using your counterFactory example with a tool like StructureMap in c# to achieve what you're doing all i need is a dependency to Lazy<Counter> or Func<Counter> and my container will actually build the factory for itself.

      Other than a "unit test" (would technically be an integration/behavioral test) where I've asked Ioc.MagicCreate<Foo> i have never once my entire life ever wrote a line of code like var c = i.instantiate(Counter); that just seems brazenly wrong and entirely ignoring the purpose of IOC. I really see no purpose to DI without IOC.

      Delete
  3. I am missing where there is not an inversion of control. Even if I ask the container to wire up a new instance for me, I'm not giving it the dependencies. Those are annotated on the service. So anywhere I need a new foo I ask for a new foo and don't care a whit about the dependencies it has. Foo also doesn't care a whit - it simply takes them in its constructor. The value is that if foo grows to depend on bar that then depends on fubar, I don't have to refactor my component that relies on foo because the DI container is in control. As for explicitly asking for an instance, that is by far the exception and not the rule. In the 80,000+ line SPA app I worked on, I can count on the fingers of one hand the times we needed to control the lifetime of a component that was in the dependency chain. Definitely a valid criticism of their approach but nothing in my mind that is earth-shattering to work around.

    ReplyDelete
    Replies
    1. "In the 80,000+ line SPA app I worked on...." ah that was the missing piece (or i just didn't read enough of the non-code portions of the article) I thought what was being discussed here was nominal as opposed to entirely atypical.

      Delete
  4. Hi, Jeremy.

    Thank you for your article.

    I was looking at the last example and saw that the 2 counters were updated differently.

    app.run(['$rootScope', 'counterFactory',
    function (rs, cf) {
    var c1 = cf.getCounter(),
    c2 = cf.getCounter();
    rs.count = c1.count;
    rs.update = c1.increment;
    rs.count2 = c2.count;
    rs.update2 = function () {
    rs.count2 = c2.increment();
    };
    }]);

    Actually, the way "rs.update" is set, the call to "c1.increment()" is made while the context is our "rs", so "c1.counter" will never be incremented (which is a bug, probably), but "this.counter++" will be executed as "rs.counter++".

    The "rs.update2" is set correctly and both counters, "c2.counter" and "rs.count2" are incremented correctly.

    So think the code should be written as follows:

    app.run(['$rootScope', 'counterFactory', function (rs, cf) {
    var c1 = cf.getCounter(),
    c2 = cf.getCounter();
    rs.count1 = c1.count;
    rs.count2 = c2.count;

    rs.update1 = function () {
    rs.count1 = c1.increment();
    };

    rs.update2 = function () {
    rs.count2 = c2.increment();
    };
    }]);

    here is my fork of your fiddle: http://jsfiddle.net/mickeyvip/rs8vw2ak/

    Thank you.



    ReplyDelete