Dependency injection: Angular dependency injection

  • Why dependency injection

  • What is dependency injection

  • How is dependency injection implemented

Understand dependency injection

There is an overview of it on the Angular website:

A dependency is a service or object that a class needs to perform its function. Dependency injection (DI) is a design pattern in which classes request dependencies from external sources rather than creating them.

Below, I will illustrate this paragraph with practical examples

1. Non-dependency injection implementation

The other day, we received a request that in the UserService UserService, we need to rely on an AuthService authorization service to verify user permissions

So easy that we can go straight to it. The code is as follows:

// auth.service.ts Authentication service
export class AuthService {
    permissions: string[];

    constructor(permissions: string) {
      this.permissions = permissions;
    }

    check(): void{... }}// user.service.ts User service
export class UserService {
    private authService: AuthService;

    constructor(permissions: string) {
      this.authService = newAuthService(permissions); }}// test.ts
const user = new UserService(['delete_user']);
Copy the code

The above is a simple implementation of our requirements. There is no problem with the functionality. We are happy to submit the code.

The next day, however, the requirements change. The authentication service also needs to provide a code parameter to check the user’s id. So you need to add the code parameter when you instantiate both UserService and AuthService so that the AuthService can get it. Even though this parameter has nothing to do with UserService. The code is as follows:

// auth.service.ts Authentication service
export class AuthService {
    permissions: string[];
    code: string;

    constructor(permissions: string; code: string) {
        this.permissions = permissions;
        this.code = code;
    }

    check(): void{... }}// user.service.ts User service
export class UserService {
    private authService: AuthService;

    constructor(permissions: string, code: string) {
        this.authService = newAuthService(permissions, code); }}// test.ts
const user = new UserService(['View user'].'ZZ0001');

Copy the code

This is just a simple example. In the actual scenario, our AuthService may also rely on other services, so if we follow the current code design, we need to pass parameters layer by layer along the link that the service depends on, so that it can run normally.

If we look at the code above, we can see that because the UserService creates an instance of The AuthService and relies on it, when we instantiate the UserService, we must also include the parameters needed in the AuthService. This is where coupling occurs.

So, can we solve this coupling problem by leaving the instantiation outside?

We tried to modify the code as follows:

// auth.service.ts Authentication service
export class AuthService {
    permissions: string[];
    code: string;

    constructor(permissions: string; code: string) {
        this.permissions = permissions;
        this.code = code;
    }

    check(): void{... }}// user.service.ts User service
export class UserService {
    private authService: AuthService;

    constructor(authService: AuthService) {
        this.authService = authService; }}// test.ts
const auth = new AuthService(['View user'].'ZZ0001');
const user = new UserService(auth);
Copy the code

Ok, with this modification, we have achieved some degree of decoupling. You don’t need to change the other services when one of them changes.

But as the system became more complex, we found that UserService also needed to rely on several services, which in turn depended on other services. Do we need to create all of its dependencies every time we use a service? This is clearly unreasonable.

Ideally, a service’s dependent services are created before it can be invoked.

Hence the idea of inversion of control.

Inversion of control and dependency injection

In Wikipedia, there is this description:

Inversion of Control (IoC) is a design principle used in object-oriented programming to reduce coupling between computer code. One of the most common is called Dependency Injection, or DI, and another is called Dependency Lookup.

Combine this with the Angular dependency injection description and practical example above:

Dependency injection (DI) is a design pattern in which classes request dependencies from external sources rather than creating them

We can summarize it briefly:

What is dependency injection?

In real development, we need to rely on other dependencies (such as AuthService) to support the execution of a class (such as class UserService). Dependencies of execution classes can be parsed and extracted by a dependency injection framework (the external source above), instantiated, and then automatically injected into the class with the result of the instantiation. Instead of creating them directly in the execution class. This is dependency injection.

What is inversion of control?

Initially, we need to manually create all of the dependencies of a class when executing it. By using dependency injection, we put the logic of creating dependencies in the dependency injection framework (DI), and the DI framework controls its creation logic, instead of manually controlling it in the business code, which is inversion of control. In short, it transfers control of “creating dependencies” from the program itself to the DI framework.

Inversion of control and dependency injection?

Inversion of control is a design principle and dependency injection is a design pattern. The ultimate goal of the system is to achieve inversion of control to achieve decoupling, and dependency injection is a means to achieve inversion of control.

Why dependency injection?

With some degree of loose coupling, we don’t need to worry that when we execute a class, its dependencies change so that the execution class also needs to change. At the same time, no other dependencies are created associated with the execution class, which also makes unit testing easier.

Second, the simple implementation of dependency injection

So how do you implement a dependency injection feature?

The idea is divided into the following steps:

1. Intercept a class before executing it

2. Extract dependencies in the execution class, including dependencies for dependent projects

3. Instantiate each dependency

4. Inject the instantiation into the execution class

Now that we have the idea, let’s look at the implementation details

1. Use reflect-metadata to manipulate metadata

First, we need a library reflect-metadata to help us with steps 1 and 2

Reflect Metadata is a proposal in ES7 for adding and reading Metadata at declaration time. When defining a class, we can use Reflect.definemetadata to store some type-related data, and then use Reflect.getMetadata to retrieve the previously defined data when we actually call the class.

First install the library

npm i reflect-metadata --save
Copy the code

Let’s use an example to briefly describe its function. If we need to define a value before executing the UserService class and use it elsewhere, you can do this:

import 'reflect-metadata';

function Injectable() {
  return function (target: any) {
    
    Reflect.defineMetadata('user_info', { name: 'kerwin' }, target); 
    return target;
  };
}

class AuthService {
    constructor(){}}@Injectable(a)class UserService {
  constructor(private authService: AuthService){}}console.log(Reflect.getMetadata('user_info', UserService)); => {name: 'kerwin'}
Copy the code

In the example above, we put the dependency as an argument to the @Injectable decorator to retrieve the dependency before executing the class. The code is as follows:

import 'reflect-metadata';

function Injectable(constructorArgs: any[]) {
  return function (target: any) {
    Reflect.defineMetadata('dependencies'.constructorArgs.target);
    return target;
  };
}

class AuthService {
    constructor(){}}@Injectable([AuthService])
class UserService {
  constructor(private authService: AuthService){}}console.log(Reflect.getMetadata('dependencies', UserService)); => [[Function: AuthService]]
Copy the code

At this point, we can retrieve the implementation class’s dependencies through the @Injectable decorator and the Reflect-Metadata library

emm… We need to write all the parameters in @Injectable. Angular doesn’t need to do that. What is the reason for this?

2. emitDecoratorMetadataThe clever use of attributes

This leads us to our next configuration item, emitDecoratorMetadata

Here is an overview of this property from the official website

With the introduction of Classes in TypeScript and ES6, there now exist certain scenarios that require additional features to support annotating or modifying classes and class members. Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.

If you want more information, consult the documentation

Here, we’ll go straight to the way of use.

Set emitDecoratorMetadata to true in tsconfig.json to enable this function.

{
  "compilerOptions": {..."emitDecoratorMetadata": true}}Copy the code

Once configured, we run the code and get the dependency directly via reflect.getMetadata (‘ Design: Paramtypes ‘, target) without adding decorator parameters. As follows:

import 'reflect-metadata';

function Injectable() {
  return function (target: any) {
    return target;
  };
}

class AuthService {
    constructor(){}}@Injectable(a)class UserService {
  constructor(private authService: AuthService){}}console.log(Reflect.getMetadata('design:paramtypes', UserService)); => [[Function: AuthService]]
Copy the code

This is similar to how we normally use Angular. But how does it work? It’s actually quite simple

Let’s set emitDecoratorMetadata to false and true, respectively, and compile ts to JS for analysis

emitDecoratorMetadata: false

"use strict";
/ /... Partial code omission
Object.defineProperty(exports."__esModule", { value: true });
require("reflect-metadata");
function Injectable() {
    return function (target) {
        returntarget; }; }...var UserService = / * *@class * / (function () {
    function UserService(authService) {
        this.authService = authService;
    }
    UserService = __decorate([
        Injectable()
    ], UserService);
    returnUserService; } ()); .Copy the code

emitDecoratorMetadata: true

"use strict";
/ /... Partial code omission
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect= = ="object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); // Reflect.metadata generates metadata for the input parameter and returns it
};
Object.defineProperty(exports."__esModule", { value: true });
require("reflect-metadata");
function Injectable() {
    return function (target) {
        return target;
    };
}

var UserService = / * *@class * / (function () {
    function UserService(authService) {
        this.authService = authService;
    }
    UserService = __decorate([
        Injectable(),
        __metadata("design:paramtypes", [AuthService]) // In the decorator, execute the __metadata method, passing "design: Paramtypes "and [AuthService] as arguments
    ], UserService);
    returnUserService; } ()); .Copy the code

Note the generated JS code and comments above, and you can see that when set to emitDecoratorMetadata: true, it automatically defines a key design: Paramtypes and value as a metadata for its dependencies in the decorator when compiling the TS file.

That’s why we’re setting emitDecoratorMetadata: After true, reflect.getMetadata (‘design: ParamTypes ‘, UserService) gets the cause of the dependency in its execution class directly without passing an argument

Ok, before executing a class, we can already get the corresponding dependency. Finally, there is the implementation of instantiation creation

3. Realize the source code

Source stamp here about the implementation code is as follows:

import 'reflect-metadata';

const providers: any[] = [];
const instanceMap = new Map(a);function Injectable() {
  return function (_constructor: any) {
    providers.push(_constructor);
    return _constructor;
  };
}

// Authentication service
@Injectable(a)class AuthService {
  checkPermission(): void {
    console.log('check permission in AuthSerivce'); }}// Local storage service
@Injectable(a)class LocalStorageService {
  save(): void {
    console.log('save user in LocalStorageService'); }}// Storage service
@Injectable(a)class StorageService {
  constructor(private localStorageService: LocalStorageService) {}

  save(): void {
    this.localStorageService.save(); }}// User services
@Injectable(a)class UserService {
  constructor(
    private authService: AuthService,
    private storageService: StorageService
  ) {
    this.authService.checkPermission();
    this.storageService.save(); }}// Create an instance
function create(target: any) {
  const dependencies = Reflect.getMetadata('design:paramtypes', target);
  const args = (dependencies || []).map((dep: any) = > {
    if(! hasProvider(dep)) {throw new Error(`${dep.name}has no provider! `);
    }

    const cache = instanceMap.get(dep);
    if (cache) {
      return cache;
    }

    let instance;
    // If the parameter has a dependency, recursively create the dependency instance
    if (dep.length) {
      instance = create(dep);
      instanceMap.set(dep, instance);
    } else {
      // Create instance directly without dependencies
      instance = new dep();
      instanceMap.set(dep, instance);
    }
    return instance;
  }) as any;
  return newtarget(... args); }// Determine the need for injection
function hasProvider(dep: any) :boolean {
  return providers.includes(dep);
}

create(UserService);
Copy the code

This is a simple implementation of dependency injection without considering many complex scenarios such as circular dependencies. Injector.ts is an implementation of Angular dependency injection, which I won’t mention here.

conclusion

Finally, we make a conclusion. Dependency injection (DI) is a design pattern that extracts and instantiates the dependencies required by the service classes in the program through the DI framework (external source), and then automatically injects them into the specified service classes. It eliminates the need to manually create instances in service classes, avoiding the high coupling between classes.

Related articles

Understand Angular dependency injection (part 2) : Dependency injection in Angular

Refer to the link

Wikipedia – Inversion of control

Dependency injection in Angular

Decorators in Typescript

How to implement inversion of control in TypeScript