In our code, a dependency is an association between two modules (like two classes) — usually one of them uses the other to do something. Use dependency injection to reduce coupling between modules, resulting in cleaner code.

Prior to the start

What is Dependency?

There are two elements A and B. Element B is called Dependency on element A if changes in element A cause changes in element B. In classes, dependencies can take many forms, such as one class sending messages to another class; A class is a member of another class; One class is an operation parameter of another class, and so on.

Why dependency Injection (DI)

Let’s define four classes: car, body, chassis, and tire. And then we initialize the car, and then we run the car.Suppose we need to change the Tire class to have a dynamic size instead of 30 all the time.Since we changed the tire definition, we need to make the following changes in order for the program to work:As you can see, just to change the tire constructor, this design needs to change the constructors of the entire upper class! In software engineering, such designs are almost unmaintainable. So we need to do itInversion of Control (IoC)That is, the top controls the bottom, not the other way around. We useDependency InjectionThis way to achieve inversion of control.The so-called dependency injection is to pass the lower class as a parameter to the upper class, so that the upper class can “control” the lower class..

Here we rewrite the definition of the car class using dependency injection passed by the constructor:Here I only need to modify the tire class, not any of the other upper classes. This is obviously easier code to maintain. Moreover, in practical engineering, this design pattern is also conducive to the cooperation and unit testing of different groups.

Environment configuration

  1. Install the typescript environment and the important Polyfill reflect-Metadata.
  2. Configure compilerOptions in tsconfig.json
{
    "experimentalDecorators": true.// Open the decorator
    "emitDecoratorMetadata": true.// Enable metaprogramming
}
Copy the code

Also introduce reflect-metadata in the entry file.

Preliminary knowledge

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.

The resources

Reflect – JavaScript | MDN

Metadata Proposal – ECMAScript

Type of access

  • Type metadata: design:type
  • Parameter type metadata: Design: Paramtypes
  • Returntype metadata: design:returntype

Method of use

/** * target: Object * propertyKey? : string | symbol */
Reflect.getMetadata('design:type', target, propertyKey); // Gets the type of the decorated property
Reflect.getMetadata("design:paramtypes", target, propertyKey); // Get the parameter type to be decorated
Reflect.getMetadata("design:returntype", target, propertyKey); // Gets the return type of the decorated function
Copy the code

The official start of the

Write modules

Start by writing a service provider as a dependent module:

// @services/log.ts
class LogService {
  publicdebug(... args:any[]) :void {
    console.debug('[DEB]'.new Date(), ...args);
  }

  publicinfo(... args:any[]) :void {
    console.info('[INF]'.new Date(), ...args);
  }

  publicerror(... args:any[]) :void {
    console.error('[ERR]'.new Date(),... args); }}Copy the code

Then we write another consumer:

// @controllers/custom.ts
import LogService from '@services/log';

class CustomerController {
  privatelog! : LogService;private token = "29993b9f-de22-44b5-87c3-e209f4174e39";

  constructor() {
    this.log = new LogService();
  }

  public main(): void {
    this.log.info('Its running.'); }}Copy the code

Now we see that the consumer instantiates the LogService in the constructor constructor and calls it in the main function, which is the traditional way to call it.

When LogService changes and modifies the constructor, and the module is heavily dependent, we have to find the references to the module one by one and change the instantiation code one by one.

Architecture design

  1. We need to use oneMapTo store registered dependencies, and itskeyIt has to be unique. So let’s first design a container.
  2. Registering dependencies is as easy as possible, and you don’t even need to define them yourselfkey, so it is used hereSymbolAnd a unique string to determine a dependency.
  3. The dependency we register doesn’t have to be a class, it could be a function, a string, a singleton, so consider the possibility of not using a decorator.

Container

Start by designing a Container class that contains ContainerMap, set, GET, and HAS properties or methods. ContainerMap is used to store registered modules, SET and GET are used to register and read modules, and HAS is used to determine whether a module is registered.

  • Set parameter ID indicates the module ID, and value indicates the module.
  • Get returns the module corresponding to the specified module ID.
  • Has is used to determine whether a module is registered.
// @libs/di/Container.ts
class Container {
  private ContainerMap = new Map<string | symbol, any> ();public set = (id: string | symbol, value: any) :void= > {
    this.ContainerMap.set(id, value);
  }
  
  public get = <T extends any>(id: string | symbol): T => {
    return this.ContainerMap.get(id) as T;
  }

  public has = (id: string | symbol): Boolean => {
    return this.ContainerMap.has(id);
  }
}

const ContainerInstance = new Container();
export default ContainerInstance;
Copy the code

Service

Now implement the Service decorator to register class dependencies.

// @libs/di/Service.ts
import Container from './Container';

interface ConstructableFunction extends Function {
  new() :any;
}

// Custom ID initialization
export function Service (id: string) :Function;

// initialize as a singleton
export function Service (singleton: boolean) :Function;

// Customize the ID and initialize it as a singleton
export function Service (id: string, singleton: boolean) :Function;

export function Service (idOrSingleton? :string | boolean, singleton? :boolean) :Function {
  return (target: ConstructableFunction) = > {
    let _id;
    let _singleton;
    let _singleInstance;

    if (typeof idOrSingleton === 'boolean') {
      _singleton = true;
      _id = Symbol(target.name);
    } else {
      // Check whether id is unique if id is set
      if (idOrSingleton && Container.has(idOrSingleton)) {
        throw new Error('Service: This identifier (${idOrSingleton}) is registered);
      }

      _id = idOrSingleton || Symbol(target.name);
      _singleton = singleton;
    }

    Reflect.defineMetadata('cus:id', _id, target);

    if (_singleton) {
      _singleInstance = new target();
    }

    Container.set(_id, _singleInstance || target);
  };
};
Copy the code

The Service is a class decorator, the ID is an optional variable to mark a module, the singleton is an optional variable to mark a singleton, and the target represents the current class to register. Once the class is acquired, add metadata to it for future use.

Inject

Next, implement the Inject decorator to Inject dependencies.

// @libs/di/Inject.ts
import Container from './Container';

// After defining a module with id, you need to inject the module with id
export function Inject(id? :string) :PropertyDecorator {
  return (target: Object, propertyKey: string | symbol) = > {
    const Dependency = Reflect.getMetadata("design:type", target, propertyKey);

    const _id = id || Reflect.getMetadata("cus:id", Dependency);
    const _dependency = Container.get(_id);

    // Inject dependencies into properties
    Reflect.defineProperty(target, propertyKey, {
      value: _dependency,
    });
  };
}
Copy the code

Begin to use

Service provider

The log module:

// @services/log.ts
import { Service } from '@libs/di';

@Service(true)
class LogService {... }Copy the code

The config module:

// @services/config.ts
import { Container } from '@libs/di';

export const token = '29993b9f-de22-44b5-87c3-e209f4174e39';

// Can be called at the entry file to load
export default () => {
  Container.set('token', token);
};
Copy the code

consumers

// @controllers/custom.ts
import LogService from '@services/log';

class CustomerController {
  // Use Inject
  @Inject(a)privatelog! : LogService;// Use container.get to inject
  private token = Container.get('token');

  public main(): void {
    this.log.info(this.token); }}Copy the code

The results

[INF] 2020-08-07T11:56:48.775Z 29993b9f-de22-44b5-87c3-e209f4174e39
Copy the code

Matters needing attention

It is possible that the decorator will be initialized before the formal invocation, so the Inject step may be performed before using the Container. Set registration (such as the config module registration and token injection above), in which case container.get can be used instead.

We can even Inject arguments inside constructor parameters and Inject dependencies directly into constructors using Inject. Of course, this needs to go down to think:

constructor(@Inject() log: LogService) {
  this.log = log;
}
Copy the code