1. An overview of the
When developing Node applications, we often encounter decorators to implement dependency injection. What is the principle behind using decorators to implement dependency injection?
When it comes to dependency injection, it’s important to mention the Ioc container and Reflect Metadata, so let’s look at these concepts in more detail
2. The Ioc container
Inversion of Control (IoC) is a core design principle in object-oriented programming. In general, most applications will rely on other objects. The original implementation process is implemented by its own program call, high coupling degree, debugging and development cost, for example, Traditional implementation:
Import {A} from './A'; import {B} from './B'; class C { consturctor() { this.a = new A(); this.b = new B(this.a); }}Copy the code
In order to reduce the coupling, the community put forward a scheme of building an object pool to maintain these dependencies, and when the application is initialized automatic processing related dependencies, and instantiate the class, the object pool which we call the Ioc container, the container to provide some basic functions, such as registration, delete, access to basic operations, such as The MIDwayJS team encapsulated the injection package on the basis of the community scheme. The relevant source code of injection is as follows:
export class ObjectDefinitionRegistry extends Map implements IObjectDefinitionRegistry { private singletonIds = []; get identifiers() { const ids = []; for (const key of this.keys()) { if (key.indexOf(PREFIX) === -1) { ids.push(key); } } return ids; } get count() { return this.size; } getSingletonDefinitionIds(): ObjectIdentifier[] { return this.singletonIds; } getDefinitionByName(name: string): IObjectDefinition[] { const definitions = []; for (const v of this.values()) { const definition = <IObjectDefinition> v; if (definition.name === name) { definitions.push(definition); } } return definitions; } registerDefinition(identifier: ObjectIdentifier, definition: IObjectDefinition) { if (definition.isSingletonScope()) { this.singletonIds.push(identifier); } this.set(identifier, definition); } getDefinition(identifier: ObjectIdentifier): IObjectDefinition { return this.get(identifier); } getDefinitionByPath(path: string): IObjectDefinition { for (const v of this.values()) { const definition = <IObjectDefinition> v; if (definition.path === path) { return definition; } } return null; } removeDefinition(identifier: ObjectIdentifier): void { this.delete(identifier); } hasDefinition(identifier: ObjectIdentifier): boolean { return this.has(identifier); } clearAll(): void { this.clear(); } hasObject(identifier: ObjectIdentifier): boolean { return this.has(PREFIX + identifier); } registerObject(identifier: ObjectIdentifier, target: any) { this.set(PREFIX + identifier, target); } getObject(identifier: ObjectIdentifier): any { return this.get(PREFIX + identifier); }}Copy the code
When the Ioc container is actually applied to objects, there are usually two forms of application: dependency injection and dependency lookup.
3. Application of Ioc
There are two main ways to implement inversion of control: dependency injection and dependency lookup. The difference between the two is that the former is passive receiving object, in the class instance creation process that creates A dependent object B, judging by type or name to different objects into different properties, while the latter is actively taking corresponding type of object, get the time dependent objects can also control in the code.
- Dependency injection has the following methods:
-
Interface-based. Implements specific interfaces for external containers to inject objects of dependent types.
-
Based on set method. Implement a public set method for a specific property to let the external container call in an object of the dependent type.
-
Based on constructors. Implements a constructor for a particular parameter that passes in an object of the dependent type when creating a new object.
-
Based on annotations. Java-based annotations, such as “@autoWired” in front of a private variable, allow external containers to pass in the corresponding object without explicitly defining the above three types of code. This scheme is equivalent to defining a public set method, but because there is no real set method, it does not expose interfaces that should not be exposed for dependency injection (because the set method only wants to be accessed by the container for injection and does not want other dependent objects to access it).
// use IoC import {Container} from ‘injection’; import {A} from ‘./A’; import {B} from ‘./B’; const container = new Container(); container.bind(A); container.bind(B);
class C { consturctor() { this.a = container.get(‘A’); this.b = container.get(‘B’); }}
- Rely on the lookup method
- Dependent search is more active. When necessary, the method provided by the framework is called to obtain the object. When obtaining the object, relevant configuration file path, key and other information should be provided to determine the state of obtaining the object
4. Introduction of decorators
Reflecting Metadata reflects Metadat is a proposal for ES7, which uses a decorator for framework languages such as Angular and Midway. It is primarily used to add and read metadata at declaration time.
A decorator is a special type of declaration. The decorator itself is a function that can be attached to a class declaration, method, accessor (getter, setter), property, or parameter. The decorator is used in the form @expression. There are class decorators, parameter decorators, method, attribute, accessor decorators. Different decorators use different scenarios and functions.
4.1 Class decorator, which declares that before the class definition, it receives the class itself as an argument, and is used to monitor, modify, or replace the class definition as follows:
Provide code address
function provide(identifier? : ObjectIdentifier) {// The default target is the adjacent DemoClass return function (target: any) {//..... Omit operation return target; }; } @provide() class DemoClass {}Copy the code
4.2 Parameter decorator, declared before a parameter declaration. The runtime is called as a function that takes three arguments:
- Target: Static properties are constructors, and instance properties are the prototype objects of the class
- Key: name of the attribute (function) called
- Index: indicates the serial number of the parameter
The usage method is as follows:
The function paramsDecorator (target: the string, the key: string, index: number) {/ / the target here, DemoClass. Propertype / / key: getList // index: 0 } class DemoClass { getList(@paramsDecorator pageId: string){// ... }}Copy the code
4.3 Accessor decorator, which declares that the runtime executes as a function before a method, property, accessor, and receives the following arguments:
- Target: Static properties are constructors, and instance properties are the prototype objects of the class
- Key: method, attribute, accessor name
- Description: Indicates the attribute description
Injection code address
function inject(identifier? : ObjectIdentifier) { return function (target: any, targetKey: string, index? : number): void { if (typeof index === 'number') { if (! identifier) { const args = getParamNames(target); if (target.length === args.length && index < target.length) { identifier = args[index]; } } const metadata = new Metadata(INJECT_TAG, identifier); tagParameter(target, targetKey, index, metadata); } else { if (! identifier) { identifier = targetKey; } const metadata = new Metadata(INJECT_TAG, identifier); tagProperty(target, targetKey, metadata); }}; } class Parent {// target: parent.propertype // key: CTX // description: PropertyDescriptor type @inject() CTX: FaaSContext; }Copy the code
5. Reflect MetaData
Reflect Metadata is a proposal in ES7 for adding and reading Metadata at declaration time. TypeScript already supports TypeScript in version 1.5+. You just need to:
npm i reflect-metadata --save
.- in
tsconfig.json
In the configurationemitDecoratorMetadata
Options.
Because the Reflect-Meta library encapsulates apis that allow access to classes or class attributes, you can manipulate classes or class attributes to add or retrieve Metadata when using decorators. Some decorators in Midway are implemented based on Reflect Metadata, such as provide
import 'reflect-metadata'; import {DUPLICATED_INJECTABLE_DECORATOR} from '.. /utils/errMsg'; import { initOrGetObjectDefProps, TAGGED_CLS } from '.. '; import {ObjectIdentifier, TagClsMetadata} from '.. /interfaces'; const camelCase = require('camelcase'); function provide(identifier? : ObjectIdentifier) { return function (target: Any) {// Check if this metadata already exists, that is, if this class is registered in the Ioc container, If (reflect.hasownMetadata (TAGGED_CLS, target)) {throw new Error(DUPLICATED_INJECTABLE_DECORATOR); } if (! identifier) { identifier = camelCase(target.name); } // If not registered, the target reflect.definemetadata (TAGGED_CLS, {id: identifier, originName: target.name, } as TagClsMetadata, target); // init property here initOrGetObjectDefProps(target); return target; }; }Copy the code
When the provide decorator is called, this class will be automatically scanned by the Ioc container and bound to the container with definitions, corresponding to container. Bind. Similarly, when the @inject is called, the definition in the container will be instantiated as an object and bound to the property. For details, please refer to inject implementation source code
Dependency Injection (DI)
With that in mind, let’s take a look at an example and the compiled code
import { provide, inject, FaaSContext } from '@ali/midway-faas'; import { InjectEntityModel } from '@midwayjs/orm'; import { Repository, DeepPartial } from 'typeorm'; import { CommonFieldStatus } from '.. /common/enum'; import { ERROR, throwError } from '.. /common/error'; import { Node } from '.. /model/node'; import { Operation, TargetType, OperationActionType } from '.. /model/operation'; import { StructureService } from '.. /service/structureService'; import { OperationService } from '.. /service/operationService'; @provide() export class NodeService { @inject() ctx: FaaSContext; @inject() operationService: OperationService; @inject() structureService: StructureService; @InjectEntityModel(Node) repository: Repository<Node>; / /... }Copy the code
After compiling:
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeService = void 0; const midway_faas_1 = require("@ali/midway-faas"); const orm_1 = require("@midwayjs/orm"); const typeorm_1 = require("typeorm"); const enum_1 = require(".. /common/enum"); const error_1 = require(".. /common/error"); const node_1 = require(".. /model/node"); const operation_1 = require(".. /model/operation"); const structureService_1 = require("./structureService"); const operationService_1 = require("./operationService"); Let NodeService = class NodeService {// logic code}; __decorate([ midway_faas_1.inject(), __metadata("design:type", Object) ], NodeService.prototype, "ctx", void 0); __decorate([ midway_faas_1.inject(), __metadata("design:type", operationService_1.OperationService) ], NodeService.prototype, "operationService", void 0); __decorate([ midway_faas_1.inject(), __metadata("design:type", structureService_1.StructureService) ], NodeService.prototype, "structureService", void 0); __decorate([ orm_1.InjectEntityModel(node_1.Node), __metadata("design:type", typeorm_1.Repository) ], NodeService.prototype, "repository", void 0); NodeService = __decorate([ midway_faas_1.provide() ], NodeService); exports.NodeService = NodeService; //# sourceMappingURL=nodeService.js.mapCopy the code