Dependency injection is an important application design pattern. Angular has its own dependency injection framework, and you really can’t build an Angular application without it. It’s used so widely that almost everyone just calls it DI.
Why dependency injection?
To understand why dependency injection is so important, consider an example without it. Imagine writing the following code:
src/app/car/car.ts (without DI)
export class Car {
public engine: Engine;
public tires: Tires;
public description = ‘No DI’;
constructor() {
this.engine = new Engine();
this.tires = new Tires();
}
// Method using the engine and tires
drive() {
return `${this.description} car with ` +
`${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
}
}
The Car class creates everything it needs inside its constructor. What’s the problem? The problem is that the Car class is brittle, inflexible, and hard to test.
This Car needs an engine and tires. Instead of asking for them, the Car constructor instantiates its own copies from the very specific classes Engine and Tires.
What if the Engine class evolves and its constructor requires a parameter? That would break the Car class and it would stay broken until you rewrote it along the lines of this.engine = new Engine(theNewParameter). The Engine constructor parameters weren’t even a consideration when you first wrote Car. You may not anticipate them even now. But you’ll have to start caring because when the definition of Engine changes, the Car class must change. That makes Car brittle.
What if you want to put a different brand of tires on your Car? Too bad. You’re locked into whatever brand the Tires class creates. That makes the Car class inflexible.
Right now each new car gets its own engine. It can’t share an engine with other cars. While that makes sense for an automobile engine, surely you can think of other dependencies that should be shared, such as the onboard wireless connection to the manufacturer’s service center. This Car lacks the flexibility to share services that have been created previously for other consumers.
When you write tests for Car you’re at the mercy of its hidden dependencies. Is it even possible to create a new Engine in a test environment? What does Engine depend upon? What does that dependency depend on? Will a new instance of Engine make an asynchronous call to the server? You certainly don’t want that going on during tests.
What if the Car should flash a warning signal when tire pressure is low? How do you confirm that it actually does flash a warning if you can’t swap in low-pressure tires during the test?
You have no control over the car’s hidden dependencies. When you can’t control the dependencies, a class becomes difficult to test.
How can you make Car more robust, flexible, and testable? That’s super easy. Change the Car constructor to a version with DI:
src/app/car/car.ts (excerpt with DI)
public description = ‘DI’;
constructor(public engine: Engine, public tires: Tires) { }
src/app/car/car.ts (excerpt without DI)
public engine: Engine;
public tires: Tires;
public description = ‘No DI’;
constructor() {
this.engine = new Engine();
this.tires = new Tires();
}
See what happened? The definition of the dependencies are now in the constructor. The Car class no longer creates an engine or tires. It just consumes them. This example leverages TypeScript’s constructor syntax for declaring parameters and properties simultaneously.
Now you can create a car by passing the engine and tires to the constructor.
// Simple car with 4 cylinders and Flintstone tires.
let car = new Car(new Engine(), new Tires());
How cool is that? The definition of the engine and tire dependencies are decoupled from the Car class. You can pass in any kind of engine or tires you like, as long as they conform to the general API requirements of an engine or tires.
Angular dependency injection
Angular ships with its own dependency injection framework. This framework can also be used as a standalone module by other applications and frameworks.
Configuring the injector – You don’t have to create an Angular injector. Angular creates an application-wide injector for you during the bootstrap process.
src/main.ts (bootstrap)
platformBrowserDynamic().bootstrapModule(AppModule);
You do have to configure the injector by registering the providers that create the services the application requires.
Registering providers in an NgModule
Here’s the AppModule that registers two providers, UserService and an APP_CONFIG provider, in its providers array.
src/app/app.module.ts (excerpt)
@NgModule({
imports: [
BrowserModule
],
declarations: [
AppComponent,
CarComponent,
HeroesComponent,
/* . . . */
],
providers: [
UserService,
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Because the HeroService is used only within the HeroesComponent and its subcomponents, the top-level HeroesComponent is the ideal place to register it.
Registering providers in a component
Here’s a revised HeroesComponent that registers the HeroService in its providers array.
src/app/heroes/heroes.component.ts
import { Component } from ‘@angular/core’;
import { HeroService } from ‘./hero.service’;
@Component({
selector: ‘my-heroes’,
providers: [HeroService],
template: `
<h2>Heroes</h2>
<hero-list></hero-list>
`
})
export class HeroesComponent { }