Solving quadruple dependency injection problem in Angular

Angular comes with built-in dependency injection and module management system, but it can get a bit tedious to use when your project grows. Especially if you use RequireJS alongside it (a must for almost any project) and also want to be minification-safe with all those dependency injections.

Let's look at a simple example with one module that provides a value and another module that prints that value:
// answer.js
define(["angular"], function(angular) {
  angular.module("the.answer")
    .value("TheAnswer", 42);
});

// main.js
define(["angular", "the.answer"], function(angular) {
  angular.module("main", ["the.answer"])
    .run(["TheAnswer", function(TheAnswer) {
      console.log(TheAnswer);
    }]);
});
As you can see, to provide a single dependency to my code, I had to specify it in four places (highlighted in red) - first as RequireJS dependency, then as Angular module dependency, after that I had to provide a guard against a minification, and finally the dependency name itself. Four places - hence the "quadruple problem".

Obviously, this goes very much against DRY principle, and leads to lots of strange errors when you forget to add or rename one of those dependencies.

Can we do better?

Let us embark on imaginary experiment. What if our js preprocessor was really smart and could actually reason about our code? Could it help us to untangle all those dependencies?

First, we can read answer.js file and see that it exports angular module "the.answer". Then, when we read main.js and see that "main" depends on "the.answer", we can automatically insert corresponding requirejs dependency. So we can drop it from our original source:
// answer.js
define(["angular"], function(angular) {
  angular.module("the.answer")
    .value("TheAnswer", 42);
});

// main.js
define(["angular"], function(angular) {
  angular.module("main", ["the.answer"])
    .run(["TheAnswer", function(TheAnswer) {
      console.log(TheAnswer);
    }]);
});
One dependency eliminated, three remaining.

We can go deeper - upon closer inspection, we can see that answer.js exports TheAnswer value in "the.answer" module. When we look at source of main.js, we see reference to TheAnswer in one of functions - thus, main module depends on it and we can skip explicit dependency declaration in angular.module() call. As a bonus, we can drop empty array from second argument on module() - it will be inserted automatically.
// answer.js
define(["angular"], function(angular) {
  angular.module("the.answer")
    .value("TheAnswer", 42);
});

// main.js
define(["angular"], function(angular) {
  angular.module("main")
    .run(["TheAnswer", function(TheAnswer) {
      console.log(TheAnswer);
    }]);
});
Now for those pesky obfuscation guards. Since we already looked through all the sources, located all created values, services and factories (and we know standard Angular dependencies like $scope or $http from documentation), we can easily determine if the function requires that obfuscation guard and wrap it automatically. In our case, we know that TheAnswer is a value provided from the.answer module, there are no other parameters to the function, so we will wrap it automatically:
// answer.js
define(["angular"], function(angular) {
  angular.module("the.answer")
    .value("TheAnswer", 42);
});

// main.js
define(["angular"], function(angular) {
  angular.module("main")
    .run(function(TheAnswer) {
      console.log(TheAnswer);
    });
});
That's a huge relief in my book - now you can't misspell or forget to add parameter to obfuscation guard, because it is handled automatically by our omnipresent imaginary compiler.

Actually, we managed to reduce original 4 dependency declarations to just one - great! But since we are in fairy world, we can let our dream fare a little bit further and add some more sugar.

Obviously, no developer in their right mind will create a variable named angular and assign it something other than actual angular instance. So we can look at the code, and if it contains identifier "angular", then we must provide angular in requirejs dependencies. Our define() headers get even smaller:
// answer.js
define([], function() {
  angular.module("the.answer")
    .value("TheAnswer", 42);
});

// main.js
define([], function() {
  angular.module("main")
    .run(function(TheAnswer) {
      console.log(TheAnswer);
    });
});
Actually, our define() headers are now basically non-existent - just an empty array of dependencies!

Since we are nice and law-abiding developers, each .js file in our source tree is probably a requirejs module, so if there is no define header, we can just add one automatically. This allows us to omit headers when we don't need them:
// answer.js
angular.module("the.answer")
  .value("TheAnswer", 42);

// main.js
angular.module("main")
  .run(function(TheAnswer) {
    console.log(TheAnswer);
  });
Isn't it nice?

Compared with our first attempt, this code is almost two times smaller and much easier to understand and edit - opportunity for introducing errors is now significantly smaller.

Well, too bad that it is just an imaginary scenario...

Or is it?

Actually, it isn't. Problems with those dependencies were hurting me bad enough, to the point that one day I wrote that omnipresent processor. Recast library was of great help - it provided javascript parsing and AST manipulation, so I only had to come up with the rules and processing logic.

You can find the resulting tool here. It is a simple Node.js-compatible script that takes source and output directories as command-line arguments and does all the magic. Even source maps are supported, allowing you to view and debug your original sources directly in browser instead of murking inside post-processed code.

Since it is mostly platform-agnostic, you can probably give it a try on any project. For example, I even managed to create a template Play! project that integrates this script into assets processing pipeline.

I enjoyed huge benefits from this tool in my Angular development recently. It's like having lisp-like macros in JavaScript, and it is in no way limited to providing dependencies - for example, you can automatically instantiate logger instances in your modules using this approach, or provide several different application builds with different features enabled or disabled.

But obviously, this is a double-edged sword - while it probably fixes lots of hard-to-catch bugs, it comes with increased infrastructure cost and will inevitably bite you, especially when you have to explain it to people new to your project. Judge for yourself :)

Comments

  1. Excuse me, how you solved the problem viewing comments posted on bloggers forum? Thank you ;)

    ReplyDelete
    Replies
    1. As far as I rememeber, they always worked more-or-less okay for me. Are you experiencing problems on your blog instance?

      Delete
  2. Yes. I posted the problem on blogger forum but do not know to help me.
    here is the problem:
    http://www.quellichelinter.it/2016/04/1-ora-stop-inter-torino.html
    Thank you;)

    ReplyDelete
    Replies
    1. There is some error inside minified code - I even have no idea how to debug this. Maybe there were already some comments on this page, with some strange content that trips the comment engine?

      Delete
    2. Thanks Rogach :). The problem had active links and deleted comments. Thanks thanks thanks....;)

      Delete

Post a Comment

Popular posts from this blog

How to create your own simple 3D render engine in pure Java

Better CLI option parsing in Scala