How dependency injection works in Angular

As our applications grow and evolves, each one of our code entities will internally require instances of other objects, which are better known as dependencies in the world of software engineering. The action of passing such dependencies to the dependent client is known as injection, and it also entails the participation of another code entity, named the injector. The injector will take responsibility for instantiating and bootstrapping the required dependencies so they are ready for use from the very moment they are successfully injected in the client. This is very important since the client knows nothing about how to instantiate its own dependencies and is only aware of the interface they implement in order to use them.

Angular features a top-notch dependency injection mechanism to ease the task of exposing required dependencies to any entity that might exist in an Angular application, regardless of whether it is a component, a directive, a pipe, or any other custom service or provider object. In fact, as we will see later in this chapter, any entity can take advantage of dependency injection (usually referred to as DI) in an Angular application. Before delving deeper into the subject, let's look at the problem that Angular's DI is trying to address.

Let's figure out if we have a music player component that relies on a playlist object to broadcast music to its users:

import { Component } from '@angular/core';
import { Playlist } from './playlist.model';

@Component({
selector: 'music-player',
templateUrl: './music-player.component.html'
})
export class MusicPlayerComponent {
playlist: Playlist;
constructor() {
this.playlist = new Playlist();
}}
}

The Playlist type could be a generic class that returns in its API a random list of songs or whatever. That is not relevant now, since the only thing that matters is that our MusicPlayerComponent entity does need it to deliver its functionality. Unfortunately, the previous implementation means that both types are tightly coupled, since the component instantiates the playlist within its own constructor. This prevents us from altering, overriding, or mocking up in a neat way the Playlist class if required. It also entails that a new Playlist object is created every time we instantiate a MusicPlayerComponent. This might be not desired in certain scenarios, especially if we expect a singleton to be used across the application and thus keep track of the playlist's state.

Dependency injection systems try to solve these issues by proposing several patterns, and the constructor injection pattern is the one enforced by Angular. The previous piece of code could be rethought like this:

import { Component } from '@angular/core';
import { Playlist } from './playlist.model';

@Component({
selector: 'music-player',
templateUrl: './music-player.component.html'
})
export class MusicPlayerComponent {
constructor(private playlist: Playlist) {}
}

Now, the Playlist is instantiated outside our component. On the other hand, the MusicPlayerComponent expects such an object to be already available before the component is instantiated so it can be injected through its constructor. This approach gives us the opportunity to override it or mock it up if we wish.

Basically, this is how dependency injection, and more specifically the constructor injection pattern, works. However, what has this got to do with Angular? Does Angular's dependency injection machinery work by instantiating types by hand and injecting them through the constructor? Obviously not, mostly because we do not instantiate components by hand either (except when writing unit tests). Angular features its own dependency injection framework, which can be used as a standalone framework by other applications, by the way.

The framework offers an actual injector that can introspect the tokens used to annotate the parameters in the constructor and return a singleton instance of the type represented by each dependency, so we can use it straight away in the implementation of our class, as in the previous example. The injector ignores how to create an instance of each dependency, so it relies on the list of providers registered upon bootstrapping the application. Each one of those providers actually provides mappings over the types marked as application dependencies. Whenever an entity (let's say a component, a directive, or a service) defines a token in its constructor, the injector searches for a type matching that token in the pool of registered providers for that component. If no match is found, it will then delegate the search on the parent component's provider, and will keep conducting the provider's lookup upwards until a provider resolves with a matching type or the top component is reached. Should the provider lookup finish with no match, Angular will throw an exception.

The latter is not exactly true, since we can mark dependencies in the constructor with the @Optional parameter decorator, in which case Angular will not throw any exception and the dependency parameter will be injected as null if no provider is found. 

Whenever a provider resolves with a type matching that token, it will return such type as a singleton, which will be therefore injected by the injector as a dependency. In fairness, the provider is not just a collection of key/value pairs coupling tokens with previously registered types, but a factory that instantiates these types and also instantiates each dependency's very own dependencies as well, in a sort of recursive dependency instantiation.

So, instead of instantiating the Playlist object manually, we could do this:

import { Component } from '@angular/core';
import { Playlist } from './playlist';

@Component({
selector: 'music-player',
templateUrl: './music-player.component.html',
providers: [Playlist]
})
export class MusicPlayerComponent {
constructor(private playlist: Playlist) {}
}

The providers property of the @Component decorator is the place where we can register dependencies on a component level. From that moment onwards, these types will be immediately available for injection at the constructor of that component and, as we will see next, at its own child components as well.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset