Angular 7: Dependency Injection

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:

{lang=js,line-numbers=off}
// 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:

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. That is, instead of using the language-provided new operator, Dependency Injection let’s us configure how objects are created.

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. This is all very abstract, so let’s dive in to some code.

How to use this chapter

This chapter is a tour of Angular DI system and concepts. You can find the code for this chapter in code/dependency-injection.

While reading this chapter, run the demo project by changing into the project directory and running:

npm install npm start

As a preview, 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.

The example code has full, runnable examples with all of the context. So if you feel lost, we’d encourage you to checkout the sample code alongside reading this chapter.

Injections Example: PriceService

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:

and then returns the final price of the Product, plus tax:

    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. Here’s how it could 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:

What should we do if we want to test the price method of the Product without relying on this external resource? In this case we often mock the PriceService. 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 also get the bonus of having confidence that 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:

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.

Let’s learn how to create our own services and the different ways of injecting them.

Dependency Injection Parts

To register a dependency we have to bind it to something that will identify that dependency. This identification is called the dependency token. For instance, if we want to register the URL of an API, we can use the string API_URL as the token. Similarly, if we’re registering a class, we can use the class itself as its token as we’ll see below.

Dependency injection in Angular has three pieces:

We can think of the role of each piece as illustrated below:

Dependency Injection

A way of thinking about this is that when we configure DI we specify what is being injected and how it will be resolved.

 
This page is a preview of ng-book 2.
Get the rest of this chapter plus hundreds of pages Angular 7 instruction, 5 sample projects, a screencast, and more.

 

Ready to master Angular 7?

  • What if you could master the entire framework – with solid foundations – in less time without beating your head against a wall? Imagine how quickly you could work if you knew the best practices and the best tools?
  • Stop wasting your time searching and have everything you need to be productive in one, well-organized place, with complete examples to get your project up without needing to resort to endless hours of research.
  • You will learn what you need to know to work professionally with ng-book: The Complete Book on Angular 7 or get your money back.
Download the First Chapter (for free)