Dependency injection in Angular 2

Original author: Pascal Precht
  • Transfer
Good evening, ladies and gentlemen!

Despite the unrelenting popularity of the AngularJS framework, we still did not manage to check out the book on its first version, but now we decided not to wait for the second one and ask: how much does this work appeal to you, covering except for AngularJS and the wider context of JavaScript development? Under the cut, you will find the translation of a regularly updated article by Pascal Precht (version dated October 12, 2015), which talks about such high matters as dependency injection in AngularJs and, most interestingly, those improvements that await this mechanism in Angular 2.





Dependency injection has always been one of Angular's most visible and trump features. So, this framework allows you to implement dependencies in various components at different points in the application; it is not necessary to know how these dependencies are created, or what dependencies they in turn need. However, it turns out that the modern dependency injection mechanism (in Angular 1) has some problems that have to be solved in Angular 2 in order to build a new generation framework. In this article, we will talk about a new dependency injection system - for future generations.

Before we start exploring new material, let's look at what “dependency injection” (DI) is and what kind of problems with DI arise in Angular 1.

Deployment of Voigta Jin’s dependencies

made an excellent report on dependency injection at the ng-conf 2014 conference. In his speech, he outlined the history of creating a new dependency injection system that will be developed for Angular 2, described the ideas on which it is based. In addition, he clearly described that DI can be considered in two ways: as a design pattern and as a framework. In the first case, the DI application patterns are explained, and in the second case, it is a system of support and dependency assembly. I’m going to build this article in the same way, since it will be easier to explain this whole concept.

To begin with, consider the following code and see what problems arise in it.

class Car {
  constructor() {
    this.engine = new Engine();
    this.tires = Tires.getInstance();
    this.doors = app.get('doors');
  }
}


Nothing special. We have a class with a constructor in which we set everything necessary to create a car object (“car”) as soon as we need it. What is the problem with this code? As you can see, the constructor not only assigns the necessary dependencies to internal properties, but also knows how to create their object. For example, the engine object (“engine”) is created using the constructor , (the bus) appears to be a single interface, and (doors) are requested through a global object that acts as a service locator . It turns out code that is difficult to maintain and even harder to test. Just imagine that you would like to test this class. How would you replace the dependency in this codeCar
Engine
Tires
doors


Engine
MockEngine
? When writing tests, we want to test various scenarios in which our code can be used, since each scenario requires its own configuration. If we need testable code, then it is implied that it will be reusable code. Which leads us to the thesis that the code under test = reusable code and vice versa.

So, how could this code be improved and made more convenient for testing? It is very simple, and you probably already understand what it is about. Change the code like this:

class Car {
  constructor(engine, tires, doors) {
    this.engine = engine;
    this.tires = tires;
    this.doors = doors;
  }
}


We just removed the creation of the dependency from the constructor and expanded the constructor function - now it counts on all the necessary dependencies. There are no concrete implementations left in the code; we literally shifted the responsibility for creating these dependencies to a higher level. If we wanted to create a car object now, we would just have to pass all the necessary dependencies to the constructor:

var car = new Car(
  new Engine(),
  new Tires(),
  new Doors()
);


Cool? Now the dependencies are separated from our class, which allows us to report moc dependencies when writing tests:

var car = new Car(
  new MockEngine(),
  new MockTires(),
  new MockDoors()
);


Imagine this is the introduction of dependency . More precisely, this particular pattern is also called " constructor injection ". There are two more patterns: implementation via the class method (setter injection) and implementation via the interface (interface injection), but we will not consider them in this article.

Great, here we are using DI, but where does the DI system come in? As mentioned earlier, we literally shifted the responsibility for creating dependencies to a higher level. This is precisely our new problem. Who will organize the assembly of all these dependencies for us? We ourselves.

function main() {
  var engine = new Engine();
  var tires = new Tires();
  var doors = new Doors();
  var car = new Car(engine, tires, doors);
  car.drive();
}


Now we need support for the main function. To do this manually is very burdensome, especially as the application grows. Maybe it’s better to do something like this?

function main() {
  var injector = new Injector(...)
  var car = injector.get(Car);
  car.drive();
}


Dependency

injection as a framework Here we begin to use dependency injection as a framework. As you know, Angular 1 has its own DI system, which allows you to annotate services and other components; also with its help the injector can find out what dependencies need to be instantiated. For example, the following code shows how to annotate our class in Angular 1:Car


class Car {
  ...
}
Car.$inject = ['Engine', 'Tires', 'Doors'];


Then we register ours as a service and each time, requesting it, we get a single instance from it, completely not bothering to create the necessary dependencies for the “car”.Car


var app = angular.module('myApp', []);
app.service('Car', Car);
app.service('OtherService', function (Car) { 
  // доступен экземпляр Car
});


Everything is cool, but it turns out that the existing DI mechanism still has some problems:

  • Internal cache - dependencies are issued as loners. Whenever we request a service, it is created within the application life cycle only once. Creating factory machinery is quite a burden.
  • Namespace conflict - there can be only one token of a particular “type” in an application. If we have a car service and a third-party extension that also introduces a service of the same name into the program, we have a problem.
  • Integration into the framework - Dependency injection in Angular 1 is built directly into the framework. There is no way to use this mechanism separately as an independent system.


These issues must be resolved to take Angular dependency injection to the next level.

Deploying Dependencies in Angular 2

Before moving on to code, you need to understand the concept behind DI in Angular 2. The following figure illustrates the required components of the new DI system.



Dependency injection in Angular 2 basically consists of three elements:

  • An injector is an object that provides us with various APIs for instantiating dependencies.
  • Provider - The provider resembles a recipe that tells the injector how to instantiate a dependency. The provider has a token that it maps to the factory function that creates the object.
  • Dependency - Dependency is the type to which the created object should belong.


So, having got an idea of ​​this concept, we will consider how it is implemented in the code. We will continue to work with our class and its dependencies. Here's how to use dependency injection in Angular 2 to get an instance :Car
Car


import { Injector } from 'angular2/di';
var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);
var car = injector.get(Car);


We are importing from Angular 2 , which provides some static APIs for creating injectors. A method is essentially a factory function that creates an injector and accepts a list of providers. Soon we will discuss how these classes are supposed to be used as providers, but for now, focus on . See how we request an instance on the last line? How does our injector know which dependencies must be created in order to instantiate a “car”? Well, consider the class ...Injector
resolveAndCreate()
injector.get()
Car
Car


import { Inject } from 'angular2/di';
class Car {
  constructor(
    @Inject(Engine) engine,
    @Inject(Tires) tires,
    @Inject(Doors) doors
  ) {
    ...
  }
}


We import the Inject entity from the framework and apply it as a decorator to the parameters of our constructor.

The decorator attaches metadata to our class , which is then consumed by our DI system. Basically, this is what we are doing here: we inform DI that the first parameter of the constructor must be an instance of type , the second of type and the third of type . We can rewrite this code in the style of TypeScript so that it looks more natural: Inject
Car
Engine
Tires
Doors


class Car {
  constructor(engine: Engine, tires: Tires, doors: Doors) {
    ...
  }
}


Well, our class declares its own dependencies, and DI can read this information and instantiate everything that is needed to create an object . But how does the injector know how to create such an object? This is where providers come into play. Remember the method in which we passed the list of classes?Car
resolveAndCreate()


var injector = Injector.resolveAndCreate([
  Car,
  Engine,
  Tires,
  Doors
]);


Again, the question may arise how this list of classes will act as a list of providers.
It turns out that this is just a shorthand syntax. If you convert it to a longer and longer version, the situation will become clearer:

import {provide} from 'angular2/angular2';
var injector = Injector.resolveAndCreate([
  provide(Car, {useClass: Car}),
  provide(Engine, {useClass: Engine}),
  provide(Tires, {useClass: Tires}),
  provide(Doors {useClass: Doors})
]);


We have a function that maps a marker to a configuration object. This token can be a type or a string. If you read these providers now, it becomes much clearer what is happening. We provide an instance of a type through a class , type through a class , etc. This is the recipe mechanism that we talked about above. So, with the help of providers, we not only tell the injector what dependencies are used throughout the application, but also describe how objects of these dependencies will be created. Now the following question arises: when is it desirable to use a longer syntax instead of a shorter one? Why writeprovide()
Car
Car
Engine
Engine


provide(Foo, {useClass: Foo})
if you can get by with one foo, right? Yes, that's right. That's why we immediately started with a shorthand syntax. However, a longer syntax opens up great, very large possibilities. Take a look at the following code snippet.

provide(Engine, {useClass: OtherEngine})


Right. We can display a marker on almost anything. Here we map the marker to the class . Thus, if we now request an object of type , we get an instance of the class . This is an incredibly powerful mechanism that allows not only to avoid name conflicts, but also to create a type as an interface, binding it to a specific implementation. In addition, we can unload this or that dependency in the right place, replacing it with a marker and not touching the rest of the code. Dependency injection in Angular 2 also introduces a couple more provider recipes, which we'll talk about in the next section. Other provider configurationsEngine
OtherEngine
Engine
OtherEngine







Sometimes we want to get not a class instance, but just a single value or a factory function, in which additional configuration may be required. Therefore, the provider mechanism for working with DI in Angular 2 provides several recipes. Let's quickly go over them.

Providing Values

You can provide a simple value with

{useValue: value}
provide(String, {useValue: 'Hello World'})


This is convenient when we want to provide simple configuration values.

Providing aliases

You can map an alias token to another token, like this:

provide(Engine, {useClass: Engine})
provide(V8, {useExisting: Engine})


Providing factories

Yes, our favorite factories

provide(Engine, {useFactory: () => {
  return function () {
    if (IS_V8) {
      return new V8Engine();
    } else {
      return new V6Engine();
    }
  }
}})


Of course, a factory can accept its own dependencies. To pass dependencies to the factory, simply add a list of tokens to the factory:

provide(Engine, {
  useFactory: (car, engine) => {
  },
  deps: [Car, Engine]
})


Optional Dependencies The

decorator allows you to declare dependencies as optional. This can be convenient, for example, in cases where our application relies on a third-party library, and if this library is not available, a rollback mechanism is needed.@Optional


class Car {
  constructor(@Optional(jQuery) $) {
    if (!$) {
    // откат
    }
  }
}


As you can see, DI in Angular 2 solves almost all the problems that existed with DI in Angular. But we have not yet discussed one issue. Are loners still being created with the new DI? Yes.

One-time dependencies and child injectors

If we need a one-time dependency (transient dependency) - one in which we always need a new instance when requested, there are two options:

Factories can return class instances. It will not be loners.

provide(Engine, {useFactory: () => {
  return () => {
    return new Engine();
  }
}})

You can create a child injector with . The child injector introduces its own bindings, and this instance will be different from the parent injector. Injector.resolveAndCreateChild()


var injector = Injector.resolveAndCreate([Engine]);
var childInjector = injector.resolveAndCreateChild([Engine]);
injector.get(Engine) !== childInjector.get(Engine);


Subsidiary injectors are interesting not only for this. It turns out that the child injector can search for the marker binding on the parent injector if no binding for the specific marker is registered on the child injector. The following diagram shows what happens:



The figure shows three injectors, two of which are daughter injectors. Each injector receives its own configuration of providers. Now, if we request an instance of type Car from the second child injector, this child injector will create a “car” object. However, the “engine” will be created by the first child injector, and the “tires” and “doors” will be created by the most superior of the external parent injectors. It turns out something like a chain of prototypes.

You can even configure the visibility of dependencies, as well as specify the level to which the child injector must look for information. However, this is a topic for another article.

How will all this work in Angular 2?

Now that we’ve covered dependency injection in Angular 2, let's discuss how this mechanism works within the framework. Should we create injectors manually when assembling Angular 2 components? Fortunately, the Angular team spared no time and effort, and created a beautiful API that hides all injector technology when assembling components in Angular 2.

Consider the following simple Angular 2 component.

@Component({
  selector: 'app'
})
@View({
  template: '

Hello !

' }) class App { constructor() { this.name = 'World'; } } bootstrap(App);


Nothing special. Suppose we want to extend this component with the help used in the component constructor. Such a service might look like this:NameService


class NameService {
  constructor() {
    this.name = 'Pascal';
  }
  getName() {
    return this.name;
  }
}


Again, nothing special. Just create a class. Then, in order to open access to it in our application as an embedded object, we must tell the injector of our application some information about the configuration of the provider. But how to do that? We have not even created an injector.

The method creates a root injector for our application during its initial loading. It takes the list of providers as the second argument, and this list will be passed directly to the injector right at the stage of its creation. In other words, here's what you need to do here:bootstrap()


bootstrap(App, [NameService]);


That's all. Now, moving on to implementation as such, we apply the decorators studied above .@Inject


class App {
  constructor(@Inject(NameService) NameService) {
    this.name = NameService.getName();
  }
}


Or, if we dwell on TypeScript, we can simply add type annotations to our constructor:

class App {
  constructor(NameService: NameService) {
    this.name = NameService.getName();
  }
}


Great! The entire Angular interior miraculously disappeared somewhere! But one more question remains: what if we need a different configuration of bindings for a particular component?

Suppose we have a service that can be implemented within an entire application for a type , but exactly one component needs a different service? This is where the annotation property comes in handy . It allows us to add providers to a specific component (as well as its child components).NameService
NameService
providers
@Component


@Component({
  selector: 'app',
  providers: [NameService]
})
@View({
  template: '

Hello !

' }) class App { ... }


Let me explain: it does not configure the instances that will be implemented. It configures the child injector that will be created for this component. As mentioned above, we can also configure the visibility of our bindings, or rather, indicate which component can implement it. For example, the viewProviders property allows access to dependencies only for the presentation of the component, but not for its descendants. Conclusion The new dependency injection system in Angular solves all the problems that existed with the DI in Angular 1. No more name conflicts. This is a separate component of the framework, can be used as a standalone system, even without Angular 2.providers




Only registered users can participate in the survey. Please come in.

JavaScript book, jQuery, AngularJS

  • 37.6% Publish, 900+ pages in translation - no big deal 35
  • 55.9% Not worth it, publish Angular 2 as 52 appears
  • 26.8% Thanks for the article! 25
  • 8.6% A lot of code from nothing, I'd rather reread Flanagan 8

Also popular now: