Dependency Injection in Angular

As our programs grow in size, parts of the app need to communicate with other modules. When module A requires module B to run, we say that B is a dependency of A. One of the most common ways to get access to dependencies is to simply import a file. For instance, in this hypothetical module we might do the following:

// in A.ts
import {B} from 'B'; // a dependency!
B.foo(); // using B

In many cases, simply importing code is sufficient, but other times we need to provide dependencies in a more sophisticated way. For instance, we may want to:
1) substitute out the implementation of B for MockB during testing
2) share a single instance of the B class across our whole app (e.g. the Singleton pattern)
3) create a new instance of the B class every time it is used (e.g. the Factory pattern)


Dependency Injection can solve these problems

Dependency Injection (DI) is a system to make parts of our program accessible to other parts of the program – and we can configure how that happens. One way to think about “the injector” is as a replacement for the new operator.
The term Dependency Injection is used to describe both a design pattern (used in many different frameworks) and also the specific implementation of DI that is built-in to Angular.
The major benefit of using Dependency Injection is that the client component needn’t be aware of how to create the dependencies. All the client component needs to know is how to interact with those dependencies.
To get Dependency Injection to work involves configuration in your NgModules. It can feel a bit confusing at first to figure out “where” things are coming from.
Example: Let’s imagine we’re building a store that has Products and we need to calculate the final price of that
product after sales tax. In order to calculate the full price for this product, we use a PriceService
that takes as input:
1) the base price of the Product and
2) the state we’re selling it to.
and then returns the final price of the Product, plus tax:

Example:

export class PriceService {
constructor() { }
calculateTotalPrice(basePrice: number, state: string) {
// e.g. Imgine that in our "real" application we're
// accessing a real database of state sales tax amounts
const tax = Math.random();
return basePrice + tax;
}
}

In this service, the calculateTotalPrice function will take the basePrice of a product and the state and return the total price of product.
Say we want to use this service on our Product model. It will look without dependency injection:

import { PriceService } from './price.service';
export class Product {
service: PriceService;
basePrice: number;
constructor(basePrice: number) {
this.service = new PriceService(); // <-- create directly ("hardcoded")
this.basePrice = basePrice;
}
totalPrice(state: string) {
return this.service.calculateTotalPrice(this.basePrice, state);
}
 }

Now imagine we need to write a test for this Product class. We could write a test like this:

import { Product } from './product';
describe('Product', () => {
let product;
beforeEach(() => {
product = new Product(11);
});
describe('price', () => {
it('is calculated based on the basePrice and the state', () => {
expect(product.totalPrice('FL')).toBe(11.66); // <-- hmmm
});
})
});

The problem with this test is that we don’t actually know what the exact value for tax in Florida (‘FL’) is going to be. Even if we implemented the PriceService the ‘real’ way by calling an API or calling a database, we have the problem that:
1) The API needs to be available (or the database needs to be running) and
2) We need to know the exact Florida tax at the time we write the test.
For example, if we know the interface of a PriceService, we could write a MockPriceService which will always give us a predictable calculation (and not be reliant on a database or API).
Here’s the interface for IPriceService:

export interface IPriceService {
calculateTotalPrice(basePrice: number, state: string): number;
}

This interface defines just one function: calculateTotalPrice. Now we can write a MockPriceService that conforms to this interface, which we will use only for our tests:

import { IPriceService } from './price-service.interface';
export class MockPriceService implements IPriceService {
calculateTotalPrice(basePrice: number, state: string) {
if (state === 'FL') {
return basePrice + 0.66; // it's always 66 cents!
}
return basePrice;
}
}

Now, just because we’ve written a MockPriceService doesn’t mean our Product will use it. In order to use this service, we need to modify our Product class:

import { IPriceService } from './price-service.interface';
export class Product {
service: IPriceService;
basePrice: number;
constructor(service: IPriceService, basePrice: number) {
this.service = service; // <-- passed in as an argument!
this.basePrice = basePrice;
 }
totalPrice(state: string) {
return this.service.calculateTotalPrice(this.basePrice, state);
}
}

Now, when creating a Product the client using the Product class becomes responsible for deciding which concrete implementation of the PriceService is going to be given to the new instance. And with this change, we can tweak our test slightly and get rid of the dependency on the unpredictable PriceService:

import { Product } from './product.model';
import { MockPriceService } from './price.service.mock';
describe('Product', () => {
let product;
beforeEach(() => {
const service = new MockPriceService();
product = new Product(service, 11.00);
);
describe('price', () => {
it('is calculated based on the basePrice and the state', () => {
expect(product.totalPrice('FL')).toBe(11.66);
});
});
});

We’re testing the Product class in isolation. That is, we’re making sure that our class works with a predictable dependency. While the predictability is nice, it’s a bit laborious to pass a concrete implementation of a service every time we want a new Product. Thankfully, Angular’s DI library helps us deal with that problem, too. More on that below.
Within Angular’s DI system, instead of directly importing and creating a new instance of a class, instead we will:
1) Register the “dependency” with Angular
2) Describe how the dependency will be injected
3) Inject the dependency
One benefit of this model is that the dependency implementation can be swapped at run-time (as in our mocking example above). But another significant benefit is that we can configure how the dependency is created.
That is, often in the case of program-wide services, we may want to have only one instance – that is, a Singleton. With DI we’re able to configure Singletons easily.
A third use-case for DI is for configuration or environment-specific variables. For instance, we might define a “constant” API_URL, but then inject a different value in production vs. development.

For any query regarding Dependency Injection in Angular, drop a comment below.

Leave a Comment

Your email address will not be published. Required fields are marked *