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. emitDecoratorMetadata
The 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