Over the last month I've been wrapping my head around Angular JS. I ended up exploring Dependency Injection, and thought I'd share my experiments.
Angular uses dependency injection everywhere. Dependency Injection, or DI, means instead of using or looking up a service in the global name space, it is passed into the code, or "injected." In the early 2000s in the Java world, when the Spring dependency injection framework came out, there was much rejoicing. A very basic example shows the principal:
console is passed in, instead of being referenced off the global object. The advantages are:
- We are not accessing some global value anymore, so the code is more modular. It can run in any environment, whether it has a global console or not. And if we want to change what console does in one part of the code and not the other, we have that level of control.
The disadvantage is simply that you need to find the service and inject it. Since passing services in everywhere becomes burdensome, DI frameworks automate this injection.
If you're familiar with the nodejs/require, you might think it's equivalent, but it's not. The require asks for a specific global resource explicitly. DI gives a level of indirection and allows plug in different resources.
What Angular Dependency Injection Looks Like
Within Angular, dependencies are injected using function parameters. This is a reasonable, functional language choice. Angular is responsible for calling the function with those dependencies injected. Let's say you have a couple services:
Here's a third service that uses them:
It's easy to define, but there's some awkwardness here: a function that returns another function? Ugh. It takes a while to be able to read through this code easily, since this pattern is nearly everywhere.
Now to call our service, we need it to be injected with its dependencies, which is also a little awkward, but accomplished with an additional function wrapper:
What Angular Dependency Injection REALLY Looks Like
Quickly you run into different ways to define functions within Angular. Since Google's Closure Compile, and many other code compressors will rename parameters to save a few bytes, this technique doesn't work. So the solution worked out was to declare an array that lists the injected variables along with the function:
This ends up looking pretty awkward to my eye, and I was wondering if there was a better way.
Well, first I had to deconstruct how Angular does it.
How the magic is implemented
How this works is interesting.
The secret behind all of this is something I found a little counter-intuitive: when you
toString on a function object, you get the actual text of the function. For example here is a useful
(Below are snippets of my code, not Angular, although much is based on techniques in Angular code.)
For DI, Angular needs to know the argument names. In order to figure this out, Angular uses regular expressions. I found other DI frameworks that build a parse tree to identify these, but in this case, a regular expression suffices. I extracted Angular logic into its own function:
The final piece is to put it together: figure out the arguments and then call the function with the correct services:
For DI, you need some way to discover the objects that are injected. I think this works well as a separate concept, so I define a stub service locator, which given a name, returns the right service:
Alternative Technique #1
So I wondered is there a more concise way to do this dependency injection.
Creating a whole new function just to set the injected variables seemed like
overkill. I started noodling... a thing about Angular you notice right off
$scope object. It's an injected parameter that is used
to communicate between the controller and views. By having
most code doesn't use the
this context object much.
Could we simply call the service with all the injected parameters
this context object?
(Yes, there are newer Angular approaches that use it more than most of the sample code,
but let's not worry about that, shall we?)
The definition of the service is simpler.
There's no need for nested functions returning functions.
Simply reference variables associated with this context
or in Coffeescript,
And the call is identical to the Angular style:
The injector itself isn't more complicated than the angular one. Using building blocks in the other parts of the code:
It depends on this function
which looks through the whole function body for references off of
(Perhaps it's naive to think that this is going to work in practice. I'm not sure.
I do realize it will falsely match nested function references,
and "over-inject". And if
this references are renamed
that, it's not going to work without modification.)
In this implementation I'm not telling it what to inject, following the Angular technique of sucking them out of the source code automatically. I see a couple advantages over Angular's:
- The definition is simpler, without functions returning functions. Although identifying injected services is harder.
- This technique is less susceptible to the drawbacks, as Google's Closure Compile will not rename properties in the SIMPLE_OPTIMIZATIONS mode.
Experiment #2: Objects
(I'm going to avoid Coffeescript's
so as to avoid any questions as to how it behaves in reference
We'll see both the object definition and the call:
We will do what we just did in Experiment #1, but once for each function of an object.
We find all the functions and then from there find all the needed services, and inject them into the object.
Experiment #3: Explicit Objects
The magic hunting around for function names isn't that comfortable, we can be explicit. In this definition of an object will simply indicate the names of the services that are required, so it's clear they need to be injected:
If you are still with me, you should be able to appreciate the implementation:
For Angular, I see how Experiment #1 is an interesting technique. But there are three, or maybe four techniques already in use there, and to be honest, and adding another wouldn't help matters. That ship has sailed. I like the technique nonetheless.
For Coffeescript or pseudo-class system, one of the object-oriented techniques will be natural. Experiment #3 would fit in very nicely with a BackBone app.
I'm sure there are other ways to do this... this is my riffing on just one direction. What do you think?