What is Dependency injection

Dependency injection is defined as the dependencies between components is determined by container at runtime, said that by the container dynamic image will be a dependency injection into components in object-oriented programming, we often deal with problem is decoupled, inversion of control (IoC) is commonly used, the design principles of object-oriented programming, including dependency injection is the realization of inversion of control is the most commonly used. The target solution is that the current class is not responsible for creating and initializing instances of dependent classes.

what is Dependency

Dependencies are A common phenomenon in programs, assuming that A and B are coupled to C dependencies, dependencies are everywhere in OOP programming. Dependencies can take many forms, such as a class sending a message to another class, a class being a member of another class, or a class being an argument to another class.

class A {}

class B {
  classA: A;
  constructor() {
    this.classA = newA(); }}class C {
  classA: A;
  classB: B;
  constructor() {
    this.classA = new A();
    this.classB = newB(); }}Copy the code

when is use Dependency injection

Eg: The user invokes the API layer to print logs

  • LoggerService is dependent on ApiService and UserService
  • ApiService is dependent on UserService
class LoggerService {
    constructor(){}log(args) {
        console.log(args)
    }
}

class ApiService {
    constructor (
        private readonly logger: LoggerService
    ) {
        this.logger.log('api constructor')}public async getMydata () {
        return { name: 'mumiao'.hobby: 'focusing in web'}}}class UserService {
    constructor (
        private readonly api: ApiService,
        private readonly logger: LoggerService
    ) {
        this.logger.log('user constructor')}async getMyhobby () {
        const { hobby } = await this.api.getMydata()
        return hobby
    }
}

async function Main {
    const loggerService = new LoggerService()
    const apiService = new ApiService(loggerService)
    const userService = new UserService(loggerService, userService)
    console.log('my hobby is'.await userService.getMyhobby())
}

Main()
Copy the code
Existing problems
  • Unit tests are difficult to write
  • Components are difficult to reuse and maintain, and the scalability is low
  • UserServiceShould not carryApiServiceandLoggerServiceInstance creation.
How to solve

With dependency injection, The UserService is not responsible for the creation and destruction of dependent classes, but is injected externally through API and Logger objects. There are three common dependency injection methods, and constructor injection is used as an example in this paper.

const apiService = Injector.resolve < ApiService > ApiService;
const userService = Injector.resolve < UserService > UserService;
// returns an instance of , with all injected dependencies
Copy the code

implement simply Dependency injection

Preliminary knowledge

  • ES6 uses relatively few features in its normal business: Reflect, Proxy, Decorator, Map, Symbol
  • Understand Dependency Injection, ES/TS decorator
  • Learn more about typescript-Reflect Metadata
Reflect
Introduction to the

Proxy and Reflect are apis introduced by ES6 to operate objects. Reflect’s API and Proxy API correspond one by one, and some object operations can be implemented functionally. In addition, using reflect-metadata allows Reflect to support metaprogramming

Type of access
  • Type metadata: design:type
  • Parameter type metadata: Design: Paramtypes
  • Function returns value type metadata: design: returnType
Reflect.defineMetaData(metadataKey, metadataValue, target) // Define metadata on the class
Reflect.getMetaData("design:type", target, propertyKey); // Returns the class decorator property type
Reflect.getMetaData("design:paramtypes", target, propertyKey); // Returns the class decorator parameter type
Reflect.getMetaData("design:returntype", target, propertyKey); // The return class is decorated with the function return value type
Copy the code
Decorators
function funcDecorator(target, name, descriptor) {
  // Target indicates that the class's prototype name is the function name descriptor
  let originalMethod = descriptor.value;
  descriptor.value = function () {
    console.log("I am the decorator logic of Func.");
    return originalMethod.apply(this.arguments);
  };
  return descriptor;
}

class Button {
  @funcDecorator
  onClick() {
    console.log("I am the original logic of Func"); }}Copy the code
Reflect and Decorators
const Injector = (): ClassDecorator= > {
  // es7 decorator
  return (target, key, descriptor) = > {
    console.log(Reflect.getMetadata("design:paramtypes", target));
    // [apiService, loggerService]
  };
};

@Injector(a)class userService {
  constructor(api: ApiService, logger: LoggerService){}}Copy the code
implement simply Dependency injection
// interface.ts

type Type<T = any> = new(... args:any[]) => T;
export type GenericClassDecorator<T> = (target: T) = > void;

// ServiceDecorator.ts

const Service = (): GenericClassDecorator<Type<object> > = > {return (target: Type<object>) = > {};
};

// Injector.ts
export const Injector = {
  // resolving instances
  resolve<T>(target: Type<any>): T {
    // resolved injections from the Injector
    let injections = Reflect.getMetadata("design:paramtypes", target) || [],
      injections = injections.map((inject) = > Injector.resolve<any>(inject));

    return newtarget(... injections); }};Copy the code

Only the core part of dependency extraction is implemented. Another part of dependency injection is related to Container storage.

Resolve Dependency
@Service(a)class LoggerService {
/ /...
}

@Service(a)class ApiService {
    constructor (
        private readonly logger: LoggerService
    ) {}}@Service
class UserService {
    constructor (
        private readonly api: ApiService,
        private readonly logger: LoggerService
    ) {}}async function Main {
    // jnject dependencies
   const apiService = Injector.resolve<ApiService>(ApiService);
   const userService = Injector.resolve<UserService>(UserService);
   console.log('my hobby is'.await userService.getMyhobby())
}

Main()
Copy the code
implement simply Dependency injection with container

APIs of InversifyJS with TypeScript

Using the step

  • Step 1: Declare the interface and its type
  • Step 2: Declare dependencies using @Injectable & @Inject decorators
  • Step 3: Create and configure a Container
  • Step 4: Parse and extract dependencies

The sample

Declare interface and type:

export interface ILoggerService {}
export interface IApiService {}
export interface IUserService {}

export default TYPES = {
  // Unique dependent identifiers. It is recommended to use symbol. for instead of class identifiers
  ILoggerService: Symbol.for("ILoggerService"),
  IApiService: Symbol.for("IApiService"),
  IUserService: Symbol.for("IUserService"),};Copy the code

Declare dependencies:

import 'reflect-metadata'
import { injectable, inject } from 'inversify'

@injectable(a)export class LoggerService implements ILoggerService{
/ /...
}

@injectable(a)export class ApiService implements IApiService{
    protected _logger: LoggerService
    constructor (
        private @inject(TYPES.ILoggerService) logger: LoggerService
    ) {
        this._logger = logger
    }
}
Copy the code

Property injection can also be used instead of constructor injection so that constructors are not declared.

@injectable(a)export class ApiService implements IApiService {
  @inject(TYPES.ILoggerService) private _logger: LoggerService;
}
Copy the code
@injectable(a)export class UserService implements IUserService {
    protected _api: ApiService;
    protected _logger: LoggerService;

    constructor (
        private readonly @inject(TYPES.IApiService) api: ApiService,
        private readonly @inject(TYPES.ILoggerService) logger: LoggerService
    ) {
        this._api = api
        this._logger = logger
    }
}
Copy the code

Create and configure a Container

.const DIContainer = new container()
DIContainer.bind<ApiService>(TYPES.IApiService).toSelf()
DIContainer.bind<LoggerService>(TYPES.ILoggerService).toSelf()
Copy the code

Analytical dependence

import "reflect-matadata";
import { UserService } from "./services";
import DIContainer from "./container";

async function Main() {
  const userService: UserService = DIContainer.resolve<UserService>(
    UserService
  );
  console.log("my hobby is".await userService.getMyhobby());
}

Main();
Copy the code
Classes as identifiers and circular dependencies

An exception:

Error: Missing required @Inject or @multiinject annotation in: argument 0 in class Dom.

import "reflect-metadata";
import { injectable } from "inversify";

@injectable(a)class Dom {
  public _domUi: DomUi;
  constructor(@inject(DomUi) domUi: DomUi) {
    this._domUi = domUi; }}@injectable(a)class DomUi {
  public _dom;
  constructor(@inject(Dom) dom: Dom) {
    this._dom = dom; }}@injectable(a)class Test {
  public _dom;
  constructor(@inject(Dom) dom: Dom) {
    this._dom = dom;
  }
}

container.bind<Dom>(Dom).toSelf();
container.bind<DomUi>(DomUi).toSelf();
const dom = container.resolve(Test); // Error!
Copy the code

InversifyJS recommends using symboy. for to generate dependent unique identifiers

inject practice frameWorks

Dependency injection (DI) is usually implemented by third-party frameworks, which consider cyclic dependencies, error handling, container storage, etc.

  • Tsyringe:Github.com/microsoft/t…
    • Practice: github.com/DTStack/mol…
  • InversifyJS: Github.com/inversify/I…
    • Practice: codesandbox. IO/s/a lot/in…