Complete guide to Angular Dependency Injection framework

Rafayel Arakelyan
17 min readApr 12, 2020
  1. Dependency Injection as application design pattern.
  2. Concept of Injectors.
  3. DI framework in Angular (Coming soon).
  4. Common misunderstandings related to Angular DI (Coming soon).
  5. Advanced techniques and flexible usage of Angular DI (Coming soon).

Dependency Injection as application design pattern

In fact, behind all the mysterious definitions and complex interpretations, the core concept of dependency injection is indeed pretty straightforward.

The dependency injection is the process of passing some objects as dependencies to the target object, so that it can use them for internal implementations.

Think of dependencies as services and the target object as a consumer. The main purpose of dependency injection is to make transparent for consumer where those services are coming from and how they work, while he knows how to use them.

Consider the code below

We have a class GoogleGeocoder. It makes network requests to fetch current coordinates as well as coordinates by specific address.

Also we have a class Navigator. It has one and only method getDistance to calculate distance to the given endpoint address.

As you can see Navigator (consumer) makes use of GoogleGeocoder (service) when computing distance, so it can’t operate without it. In other words Navigator depends on GoogleGeocoder or, which is the same, GoogleGeocoder is a dependency of Navigator.

In this particular example, consumer itself generates the service, to consume it later on. This behavior is a direct contradiction of dependency injection pattern. But before even touching this code, let’s first take a look on issues that it can lead to, and how dependency injection can address them.

1. Inflexible

Class Navigator is tightly coupled with GoogleGeocoder. What if we decide to use some other service for determining coordinates? We have to modify Navigator class to create that service instead. What if we want to use specific service depending on environment or some configuration? What if we need to create several navigators each with a different geocoder service? Indeed, It will be impossible. So this code is not configurable and hard to reuse.

2. Untestable

Feature testing is usually achieved by creating same thing in different contexts with some dummy data, and expecting desired behavior for each case. To test Navigator, we may want to create one and call getDistance method to ensure that it actually returns some number.

But wait, doing so we will initiate real GoogleGeocoder network requests which is of course, not what we want.
First, We don’t want extra network requests when testing.
And second, If we are using paid API key to fetch coordinates, this way our tests may easily exhaust the whole service quota.

The solution here is to use mock geocoder that will imitate similar behavior with static or random results, rather than making real requests. But since we have hardcoded GoogleGeocoder, we can’t configure Navigator to use mock geocoder when testing.

3. Hard to maintain

As a rule, services are designed to be used throughout the application in many places. So imagine you have dozens of classes like Navigator, using GoogleGeocoder service. And all of them adopted the same manner to create the service on their own.

Now, let’s assume at some point GoogleGeocoder has been updated to require an API key as a parameter of constructor - new GoogleGeocoder(API_KEY).
Or even worse, maybe we completely changed the way we create it. Perhaps now, instead of new keyword, we use GoogleGeocoder.create() static method to create an instance.
So how this will affect our code?

At this point we can say the following. Everything that concerns GoogleGeocoder also concerns to its consumers, since they create the service manually. If we made any changes on how GoogleGeocoder instance should be created, we need to modify all dependent classes one by one to make sure they create it correctly as well. So the more classes depends on our service the harder becomes to maintain it safely.

All the mentioned issues above can be considered as consequences of violating single responsibility principle. Navigator knows too much about the service it will use. Fortunately there is a super simple yet very effective mechanism.

Introduction to Dependency Injection

Here is how looks like simplest flow of dependency injection. It’s clearly visible that now we have no tight coupling between service and the consumer. Consumer doesn’t create anything specific on its own. Instead, it expects any ready to go service as an argument and then assigns it to some instance field which will hold a reference to the injected dependency.

With this single manipulation, we are absolutely ready to challenge all the problems discussed above. Let’s see one by one.

1. Flexible

Pay attention, any time Navigator is created we decide in place which service it should use to fetch coordinates. The consumer itself now has nothing to do with that choice. All it cares is that any injected service should at least have methods getCurrentCoordinates and getAddressCoordinates, also known as interface implementation. As you can see YandexGeocoder and BingGeocoder do implement that interface. So they can safely be used as valid geocoding services for Navigator.

It’s obvious that this way class Navigator can be used in much more flexible and reusable fashion.

2. Testable

Since we are free to use any valid class as geocoder service, it will be a good idea to define one for testing purposes.

Note, MockGeocoder slightly differs from geocoders defined earlier.

  1. It has two optional parameters in the constructor. We can pass them as static coordinates to return from getCurrentCoordinates and getAddressCoordinates methods.
  2. It has one additional method generateRandomCoordinates to avoid code duplication in two other methods.
  3. Implementation of getCurrentCoordinates and getAddressCoordinates doesn’t perform any network requests. Results are either static or completely random.

So, will those differences violate the flow somehow.
The short answer is No.

  1. Navigator has nothing to do with service construction anymore. So it doesn’t care what parameters should or can be passed to the service upon creation.
  2. Injected service should implement specific interface, that is, contain at least all the methods Navigator will use. The key word here is “at least”. So additional methods don’t bother us anyhow.
  3. What about meaningless implementation, Navigator has no idea how coordinates are figured out. It just expects them to be an object with lat and lng properties. This is exactly what we get.

Note:
In real life, we must also take care of asynchronicity in tests if needed. As long as real service performs network request and returns async data, we must imitate this behavior in mock service as well, using something like Promise.resolve(coordinates)

But for this particular section, I treat all the async flow as synchronous to keep things simple and focus on our topic.

Now, we have a mock geocoder as an ideal candidate for testing Navigator.

3. Easy to maintain

With dependency injection, consumer classes are no longer participate to service generation process. So we are free to change all the service construction rules and consumers wouldn’t even know about it. In other words we can safely maintain our dependencies without having to touch dependent classes at all.

So far so good, with just one minor change we gained so much advantages.
It can’t be that easy right?
Well, there is no magic about it. One may even compare this with ordinar function parametres. Indeed, this will be sensible assumption.

The thing is, that a curious reader will ask the following:
What is the point of all this, if we still need to maintain the code imperatively.

Playing with dependencies may still be really painful. It’s true, now we don’t have to modify consumer classes directly, but instead, we do need to modify all the points in code where we injected those dependencies.

Remember the case mentioned before. GoogleGeocoder has updated its API. Now in order to create an instance we should use static method GoogleGeocoder.create() Instead of new keyword. How we actually maintain this change?

As you can see, it’s quite trivial. We simply go through every point where the service was injected and update the way we did it.

One more example. As of now we have ability to use any valid service as a geocoder for Navigator. So let’s say we have a task to migrate from YandexGeocoder to BingGeocoder for some reason. That is to make all navigators using Yandex, to use Bing instead.

Again, same story!
We simply find all the points in code, where YandexGeocoder was injected as a service, and replace it with BingGeocoder.

These examples are for demonstration only, thus, very exaggerated. They have a lot of simple workarounds.

Factory functions alternative

We could simply write a factory function for each service. Those functions will decide which service to create as well as how to do it.
This way we will mimic Factory pattern.

Now let’s cope with same two chalanges in one go.
1. GoogleGeocoder has to be created with static method create.
2. Migrate from YandexGeocoder to BingGeocoder.

As you can see factory functions are holding all the logic. And in case of any dependency related issue, we can support it from one single place.

Dependency graph

Actually this is a worth to mention alternative. But don’t forget that we are dealing with dummy app.
What we have right now is one consumer class Navigator and several variations of Geocoder service. The dependency grapfh of our app could be illustrated as follows.

It’s extremely simple, that’s why we succed so well with factory functions. But didn’t you guess that dependencies, in turn, can have their own dependencies, and so on and so forth. This chain can get more and more.

Note:
Practically, this nesting level is infinite.
Though technically, It will be restricted when nesting level exceeds Maximum call stack size or in case of circular dependencies.

So in real life there are more complex situations that can’t be handled that easy with few factory functions.

And just to make sure you get me right, here is another dependency graph.

Note:
It’s a real dependancy graph of real project. The name and nature of the project can’t be published for sake of security.

It’s obvious that any technique mentioned before will not gonna resolve this graph. We seek for some special tool to collect all the rules, configurations and contexts in one room and control the whole flow from there. Where every object will know exatly both, what are his dependencies and who depends on him.

This is where our mechanism could be populated with one more abstraction layer called Injector.

Concept of Injectors

The injector, as such, is not an integral part of dependency injection pattern. However, you probably won’t find any framework or project using dependency injection without injectors.

Managing dependencies without injectors can be quite verbose. On the other hand, with injectors involved, we delegate all this work to some encapsulated area, somewhere behind the scenes . This helps us to accomplish so called Inversion of Control (IoC).

Let’s extend our dependency graph from previous section, to create more spacy playground.

Don’t be lazy, explore the above structure carefully.
As of now we have 5 classes in our app and many additions.

  • As before, Navigator uses Geocoder to fetch coordinates.
  • Navigator also uses Calculator to round up distance.
  • Navigator has new capabilities to get distance in various units, for this it uses Converter.
  • Geocoder uses Parser for two things.
    Parse coordinates coming from network.
    Parse raw address before requesting its coordinates.
  • Converter uses Calculator to convert units based on static formulas.
  • Calculator has calculation methods and no dependency.
  • Parser has parsing methods and no dependency.

So for each class we have following dependencies:

  • NavigatorGeocoder Converter Calculator
  • GeocoderParser
  • ConverterCalculator
  • ParserNone
  • CalculatorNone

Dependency graph will now have a bit more complex look.

Alright, let’s try to create an instance of Navigator.

const navigator = new Navigator(
new Geocoder(new Parser()),
new Converter(new Calculator()),
new Calculator(),
)

It is ridiculous, isn’t it?
And yes, this is still simple enough app in terms of DI.

It will be super to have some function which will create an instance of target class, resolving all its dependencies behind the scenes.
something like create(Navigator) or create(Geocoder).

In fact, that is exactly what injectors are meant to do. Create and provide an instance of requested class, injecting required dependencies along the way.

Injector implementation

There is no standard or preferred implementation of injector. There are plenty of them out there, from basic to more complex and flexible. Nevertheless, some main concepts are essential, that any injector consists of.
We’ll get started from something simple, adding more and more capabilities along the way.

Phase 1 - Dependency resolution

Dependency resolution is the process of finding and resolving relevant dependencies according to some predefined rules.

As those predefined rules, we will add some kind of dependency annotations for each and every class.

As you can see, all the classes were populated with new static field deps.
It holds an array of dependencies the class needs. The Injector, we are about to create, gonna use those annotations to perform dependency resolution.

See, now we don’t do any explicit injection. All we do is requesting an instance of relevant class by calling injector.get([Class]). The get method handles dependency injection for us under the hood. But how this even work?

What we are passing to get method is the class itself. Not an instance of a class, nor something else. The class itself.
This way we have access to[Class].deps static field as well as
new [Class]() signature.

To get this easy, go ahead and trace the execution of injector.get(Navigator) call. At first sight it will end up with something like this.

With help of recursion, get function calls itself again and again to extract nested dependencies all the way down, untill he reach the point where deps field is empty.

Look at the final result. We got exactly what we had when trying to instantiate Navigator by hand earlier in this section.
With just one function, we abstracted all this dirty work.

And this concerns to all classes as long as they correctly list their dependencies in deps. Any such class can be resolved with injector just like Navigator did - Injector.get([Class]).

Phase 2 - Tokens and Providers

Seems like we’re good, we have reduced so much boilerplate code. However, we got the same hidden disadvantage as in the very beggining.
Those dependency annotations in deps fields are strictly hardcoded.
Let me explain…

Currently, for each type of operation we have exactly one class.
Geocoder for geocoding,Converter for conversion and so on.
But things can change.
Remember, back then we had various geocoders - Google Yandex.
Let’s switch to it.

So which geocoder should be declared in Navigator.deps?
Well, whichever we choose, injector will inject that particular class when resolving Navigator dependencies. We know that Navigator works the same with both geocoders, so it will be sematically incorrect to say that it depends on GoogleGeocoder or YandexGeocoderexactly.
We’d like to say that Navigator depends on any valid Geocoder.
The question is, how we put this semantics into code?
Meet, Tokens and Providers.

  1. Tokens: are unique identifiers that we pass to injector to request for an associated dependency.
  2. Providers: are set of rules that hold these associations.

In fact, almost any value can be used as a token. It’s just a symbolic representation of something that has several implementations, just like abstract classes. The simplest version of token would be a string literal, so let’s go for it for now.

What would be the token to represent the two geocoding services as a whole. Perhaps, it would be "geocoder". Just a string literal of descriptive name.
Now let’s go ahead and put this token into Navigator.deps field.

This code is pretty readable and descriptive. Roughly speaking, It says
I am navigator, I need some geocoder to operate, no matter which one.
This way, we avoid hardcoding things.

But how injector gonna resolve this tokens. We need some association between the token and the actual class that will be injected. Those associations are embedded into injector as providers.

Providers can appear in various forms, depending on how the injector internally parses and reads them. For now, we simply define it as an array of two items [Token, Class].

Don’t think about how all this works, we’ll get there. Think about the logic you see. We define a generic token for each group of analogic classes. Afterwards, when creating an injector, we pass providers for those tokens. This way we configure each injector to resolve same tokens into certain classes of our choice. No hardcoding, now it’s all up to providers.

Before moving any further, let’s enhance our app a bit.
As of now we have two geocoders - GoogleGeocoder YandexGeocoder.
The same way we could have various Parsers and various Converters.

So many new things - set of parsers, set of converters, new tokens…Everything becomes much clearer when we take a look on injector implementation.

For those who are not familiar with Object.fromEntries(), it is a new static method of Object class, revealed in ES2019.
It gets a multidimensional array with key value pairs, and turns it into an object.
input: [ ['k1', 'v1'], ['k2', 'v2'] ]
output: { k1: 'v1', k2: 'v2' }

Here is a link for more details at MDN.

First of all, In the constructor, injector combines the set of providers into a single keyvalue object.
One may wonder, why we use such an odd form of providers [Token, Class] if eventually we parse them into an object. Couldn’t we just pass an object straight away and do nothing in constructor. something like…

const injector = new Injector({
calculator: Calculator,
parser: RecursiveParser,
converter: StaticConverter,
geocoder: YandexGeocoder,
navigator: Navigator
})

Well, that is a good question, but things are not as simple as they may appear. we’ll get to it very soon.

For now though, let’s talk about the get method. It has almost the same implementation as before with one small yet very important difference. Pay attention to prepended row.

const object = this.providers[token]

Before tokens and providers this function dealt with classes directly. It received and processed them right away. As of now, what we receive is a token, and we must find the class by token first - this.providers[token]. If we split apart the execution of injector once again, we’ll see the entire picture.

Now we have more control over how injector resolves dependencies. We can configure each injector with its own unique list of providers.

Even though strings are totally fine to be used as a token, you probably want to get rid of them as soon as possible. First of all there is always a chance to make typos when using them, also they cannot guarantee absolute uniqueness that you need when dealing with tokens. But there is a more important reason why we need to choose something else rather then string.

Now, Initially we said that tokens help us to avoid any hardcoding. They are meant to serve as an abstract reference to some group of classes with the same interface.

For example:
'geocoder' - GoogleGeocoder, YandexGeocoder
'parser' - RecursiveParser, LinearParser
'converter' - StaticConverter, DynamicConverter

But the thing is that injector needs a token/provider for each and every class that is used as a dependency or/and has dependencies.
So it turns out that we need to declare tokens even for those classes that has no variations at all, as long as they are a part of our DI chain.

For example:
'navigator' - Navigator
'calculator' - Calculator

It looks like unnecessary label, we don’t want to declare an extra token for a standalone class that has one and only implementation. The trick we could do is to use a class as a token for itself. To be able to do so, we need to slightly alter the way injector parses and reads providers.

As you probably know, in Javascript, plain objects can have only string keys. However, there is a special data structure called Map which allows us to store values under keys of any type. That is exactly what we need to be able to use classes as tokens. So we changed the way injector stores and reads its providers. Now it uses Map instead of plain object.
By the way, here is the reason why we pass providers as set of [Token, Class] arrays. That is the form of input that Maps are getting upon initialization.

As now we are able to use anything as a token, we got rid of unnecessary tokens like 'navigator' and'calculator' replacing them with actual classes.

To take this effect further, we need to do something similar with all other tokens as well,'parser' 'converter' and 'geocoder'. But they are different, they refer to several classes at the same time, not just one. So what exactly they should be replaced with?

To cope with this we need to create some base that will combine all the classes with same interface. In programming, we do so by using something called abstract class.

Note:
Javascript itself doesn’t support abstract classes like other object oriented programming languages do (Java, C).
So, in this particular example, we will use Typescript to demonstrate things as accurate as they can get.

So what we just did. We declared some base abstract classes to combine our groups of services together in some way. These classes themselves are not capable to be instantiated, they just serve as something that can be either extended or implemented.

Note:
Actually, abstract classes can be used in much more powerful ways. They can have not only abstract methods but also non abstract methods with predefined implementation. Those methods then can be altered or completely overriden by child classes using keywords like extends , super and so on.

Here though, we used them just like ordinar Interface. So why we didn’t actually used Interface instead?
Well, the whole point of our mission, is to extract some value to use it as a token when configuring Injector. The thing is that Interface is something that refers as a type, not value, as opposed to abstract classes.

So, at the end of the day, with all this modifications in place, we could rewrite our DI structure as follows.

The involvement of abstract classes makes the DI mechanism truly reliable. On the one hand, they serve as a unique abstract token for certain group of related classes, on the other hand, they serve as an interface that those classes have to implement in order to be providable through that token. This is some kind of two-way protection.

Well, we did a great job, but indeed these are far not all the techniques of how tokens and providers can be used. We will cover them much deeper from Angular point of view in the next section.

Phase 3 - Singleton Injectors

As a design pattern, singleton paradigm is mainly used to share state between several instaces of the same class. Singleton classes behave non traditional way in that they return the same exact instance on each instantiation instead of creating brand new instance with its own encapsulated state.

Now, from injectors point of view, singleton means that injector will always return the exact same instance of provided class for a given token.

Injector.get(Navigator) === Injector.get(Navigator) // true
Injector.get(Calculator) === Injector.get(Calculator) // true

There is a subtle nuance to note here though. Statements above doesn’t require our classes to be singleton themselves.
The fact that Injector.get([Class]) === Injector.get([Class]) doesn’t imply in any way that new [Class]() === new [Class]().

In other words singleton injector doesn’t mean to use singleton classes as providers. Instead, the injector itself should create its own local singletons out of given providers. This way every injector will have its own set of singleton providers, and it makes total sense.

Some specific behavior was applied to get method. After resolving a token to a provider it also stores that very provider instance in a dedicated Map called resolvedProviders. This way subsequent requests for the same token doesn’t perform dependency resolution over and over again, instead they grab the exact same instance that was created and stored before.

As you can see all the resolved providers including their dependencies are always pointing to the same instace. This way you can use the service to share the state between its customers.

However this is only true for each injector separately. Same provider in different injectors point to different instances. Every injector should have its own “scope” of singleton providers, and this is by design.
You may miss the importance of this detail currently, fair enough.

The thing is that in practice, there is no point to create more then one injector side by side, here we do so just for demonstration purposes.
In real life injectors tend to be nested into each other forming some sort of Injectors chain, also known as Injectors hierarchy.

Phase 4 — Injectors Hierarchy

You can think of injectors chain as a prototype chain in javascript, they don’t work exactly the same but both has the inheritance concept in mind.

Objects in javascript can be linked to other objects through special `__proto__` key. Whenever you’re trying to read a property on an object, it first tries to find it within himself, and in case there is no such a property, the lookup will be delegated to the next object under `__proto__` key.
That object in its turn can fail too, and again will delegate the lookup all the way up through the prototype chain. Once the lookup reaches the top of the chain and still no such property found, only then the value of that property would be considered as undefined.

Kind of same story with injectors chain.
Injector can have a parent injector, to which it will delegate when it fails to resolve certain token.

--

--