2

What is Dependency injection

Dependency injection is defined as the components is determined by the container at runtime, which means that the container dynamically injects a certain dependency into the component. In object-oriented programming, the problem we often deal with is decoupling , Inversion of Control (IoC) is a commonly used design principle of object-oriented programming, and dependency injection is the most commonly used implementation of Inversion of Control. The goal is to solve that the current class is not responsible for the creation and initialization of dependent class instances.

What is Dependency

Dependency is a common phenomenon in programs. Assuming that A and B are C , dependencies are everywhere in OOP programming. There are many forms of dependency. For example, one class sends a message to another class, one class is a member of another class, and one class is a parameter of another class.

class A {}

class B {
  classA: A;
  constructor() {
    this.classA = new A();
  }
}

class C {
  classA: A;
  classB: B;
  constructor() {
    this.classA = new A();
    this.classB = new B();
  }
}

When is use Dependency injection

eg: Use the user to call the API layer to print the log to illustrate

  • 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()
The problem
  • Unit tests are hard to write
  • The components are not easy to reuse and maintain, and the scalability is relatively low
  • UserService should not carry the ApiService and LoggerService instances.
How to solve

With dependency injection, UserService not responsible for the creation and destruction of dependent classes, but instead api and logger objects from the outside. There are three common dependency injection methods. This article mainly uses constructor injection as an example to explain.

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

Implement simply Dependency injection

Prerequisite knowledge

  • Feature that is relatively less used in ordinary business of ES6: Reflect, Proxy, Decorator, Map, Symbol
  • Understand Dependency injection, ES/TS decorator
  • Deep understanding of TypeScript-Reflect Metadata
Reflect
Introduction

Proxy and Reflect are APIs introduced by ES6 to manipulate objects. Reflect's API and Proxy's API correspond one-to-one, and some object operations can be implemented functionally. In addition, using reflect-metadata allows Reflect to support meta-programming

Type acquisition
  • Type metadata: design:type
  • Parameter type metadata: design:paramtypes
  • Function return value type metadata: design:returntype
Reflect.defineMetaData(metadataKey, metadataValue, target) // 在类上定义元数据
Reflect.getMetaData("design:type", target, propertyKey); //返回类被装饰属性类型
Reflect.getMetaData("design:paramtypes", target, propertyKey); //返回类被装饰参数类型
Reflect.getMetaData("design:returntype", target, propertyKey); // 返回类被装饰函数返回值类型
Decorators
function funcDecorator(target, name, descriptor) {
  // target 指 类的prototype name是函数名 descriptor是属性描述符
  let originalMethod = descriptor.value;
  descriptor.value = function () {
    console.log("我是Func的装饰器逻辑");
    return originalMethod.apply(this, arguments);
  };
  return descriptor;
}

class Button {
  @funcDecorator
  onClick() {
    console.log("我是Func的原有逻辑");
  }
}
Reflect and Decorators
const Injector = (): ClassDecorator => {
  // es7 decorator
  return (target, key, descriptor) => {
    console.log(Reflect.getMetadata("design:paramtypes", target));
    // [apiService, loggerService]
  };
};

@Injector()
class userService {
  constructor(api: ApiService, logger: LoggerService) {}
}
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 new target(...injections);
  },
};

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

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

@Service()
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()
Implement simply Dependency injection with container

injection.png

APIs of InversifyJS with TypeScript

Steps for usage

  • Step 1: Declare the interface and type
  • Step 2: Declare the dependency to use @injectable & @inject decorators
  • Step 3: Create and configure a Container
  • Step 4: parse and extract dependencies

Example

Declare the interface and type:

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

export default TYPES = {
  // 唯一依赖标识,建议使用Symbol.for替换类作为标识符
  ILoggerService: Symbol.for("ILoggerService"),
  IApiService: Symbol.for("IApiService"),
  IUserService: Symbol.for("IUserService"),
};

Declare dependency:

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

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

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

You can also use property injection instead of constructor injection, so you don't need to declare a constructor.

@injectable()
export class ApiService implements IApiService {
  @inject(TYPES.ILoggerService) private _logger: LoggerService;
}
@injectable()
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
    }
}

Create and configure a Container

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

Resolve dependencies

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();
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()
class Dom {
  public _domUi: DomUi;
  constructor(@inject(DomUi) domUi: DomUi) {
    this._domUi = domUi;
  }
}

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

@injectable()
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!

The main reason: when the decorator is called, the class has not been declared, which leads to inject(undefined) . InversifyJS recommends using Symboy.for to generate a dependent unique identifier

FrameWorks

Dependency injection is generally implemented with the help of third-party frameworks, and implementation needs to consider circular dependencies, error handling, container storage, etc.

Practice: https://github.com/DTStack/molecule

Practice: https://codesandbox.io/s/github/inversify/inversify-express-example/tree/master/?file=/BindingDecorators/controller/user.ts

author information

mumiao.png


袋鼠云数栈UED
277 声望34 粉丝

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。