The basic concept

fromWiKiExplanation of IoC

Inversion of Control (IoC) is a design principle used in object-oriented programming to reduce coupling between computer code. The most common method is called Dependency Injection (DI). Another method is called Dependency Lookup. With inversion of control, when an object is created, references to the objects it depends on are passed (injected) to it by an external entity that regulates all objects in the system.

To recap: Traditionally, developers write the program call framework (other programmers write it); IoC lets the framework call the developer’s code. IoC is a rule (or principle), and my understanding is that it does not belong to a development Pattern because it is a more universal concept. A development pattern, however, is an implementation of a law (or best practice). For example, Dependency Injection is an IoC implementation, as is the Factory pattern (or abstract Factory).

Another big application area for IoC is TDD (Test-driven Development), a testing approach based on IoC concepts.

fromWiKiExplain to DIP

The Dependency Inversion Principle (DIP) is a specific form of decoupling (traditional dependencies are created at high levels, while specific policy Settings are applied to lower level modules) so that higher level modules do not depend on the implementation details of lower level modules. The dependencies are reversed (reversed) so that the lower-level modules depend on the requirements abstraction of the higher-level modules.

The dependency reversal principle is one of the principles of SOLID (where D). Its uncoupling emphasizes that the high-level class (the caller) does not depend on the low-level class (the called).

Case code

For example, building access control system has fingerprint lock, password lock, brush face lock and magnetic card lock, an access control system contains, a door, a door with a lock, a verification agency. A more traditional approach (strong coupling) would look like this:

/ / lock mechanism
class Locker {}
/ / the door
class door {}
// Fingerprint sensor
class FingerPrintSensor {
  readFingerPrint(){}}/ / keyboard
class PasswordKeyboard {
  readInput(){}}/ / camera
class Camera {
  takePicture(){}}/ / card reader
class RFIDReader {
  readCard(){}}// Fingerprint access control
export class FingerPrintGuard {
  fingerPrintSensor = new FingerPrintSensor();
  locker = new Locker();
  docker = new Door();
}
// Password access control
export class PasswordGuard {
  keyboard = new PasswordKeyboard();
  locker = new Locker();
  docker = new Door();
}
// Swipe the face
export class FacialGurad {
  camera = new Camera();
  locker = new Locker();
  docker = new Door();
}
// Magnetic card access
export class IDGuard {
  reader = new RFIDReader();
  locker = new Locker();
  docker = new Door();
}
Copy the code

Using the DIP principles to refactor the code, we had to abstract the low-level classes FingerPrintSensor, PasswordKeyboard, Camera, and RFIDReader into an abstract class, Guarder. JS does not support abstract keywords, so write code with normal classes and throw exceptions. Remove the dependency on the high-level *Guard class.

export class Guarder {
  getKeyInfo() {
    throw "Implement validation input"; }}Copy the code

Extend this abstract class with the above four classes

// Fingerprint sensor
class FingerPrintSensor extends Guarder {
  readFingerPrint() {}
  getKeyInfo = this.readFingerPrint;
}
/ / keyboard
class PasswordKeyboard extends Guarder {
  readInput() {}
  getKeyInfo = this.readInput;
}
/ / camera
class Camera extends Guarder {
  takePicture() {}
  getKeyInfo = this.takePicture;
}
/ / card reader
class RFIDReader extends Guarder {
  readCard() {}
  getKeyInfo = this.readCard;
}
Copy the code

At this time, the high-level access control class will be changed as follows:

class Guard {
  guarder = new Guarder();
  locker = new Locker();
  docker = new Door();
}
Copy the code

Because JS is a weakly typed language, it’s easier to understand if you use TS or some other high-level language; It is not difficult to see from the above code that we have unwrapped access and access features. Similarly, to understand the DEPENDENCY injection (DI) approach to IoC principles, with a few changes to Guard (typescript)

class Guard {
  locker = new Locker();
  docker = new Door();
  constructor(guarder:Guarder) {
    this.guarder = guarder; }}Copy the code

The difference between

The purpose is to understand the design principles that occasionally arise that IoC and DIP have in common. The differences between the two are:

  • IoC removes the dependency from the aspect of program flow control
  • DIP starts from the design of the class, handles the dependency between the call and the called object, and adds an intermediate section (abstract class).

The two are not contradictory in nature, most of the content is the same.

IoC implementation Pattern

DI is used to implement IoC

Dependency injection means that it gives the caller what it needs. A dependency is something that can be called by a method. In the form of dependency injection, the caller no longer refers directly to using “dependency”, but instead to “injection”. Injection is the process of passing a dependency to the caller. After injection, the dependency is called by the caller. Passing dependencies to the caller, rather than having the caller acquire the dependencies directly, is a fundamental requirement of this design.

The example above simply uses the constructor to inject the dependent object (class), but there are other ways to do so, such as using a dedicated binding function. You can also use a more popular and elegant way to inject decorators. If you are interested, please refer to an article I wrote before.

class Guard {
  @Guarder // Attribute decorator
  guarder = {};
  locker = new Locker();
  docker = new Door();
}
Copy the code

With this development pattern, the underlying framework can design a number of decorators, while the specific logic is handled by the code in the calling aspect.

Other ways

The Service Location pattern (SL) is used less often in the otherwise weakly typed JS (but more often in Java or other high-level languages, especially in large frameworks). The idea is to load the dependent classes into a dictionary (or Map) and then use a configuration file or constant to implement the reference process using a service loader when it needs to be called. In order to achieve some degree of uncoupling. Let’s take the case of entrance guard to illustrate:

// Service locator (class)
class ServiceLocator {
  static sInstance = {};
  static load(arg) {
    ServiceLocator.sInstance = arg;
  }
  services = {};
  loadService(key, service) {
    this.services[key] = service;
  }
  static getService(key) {
    console.log(key, ServiceLocator.sInstance.services[key]);
    returnServiceLocator.sInstance.services[key]; }}// Access control can be loaded according to the configuration
class Guard {
  guarder = ServiceLocator.getService("Camera");
  locker = new Locker();
  docker = new Door();
}

// Registration service
(function () {
  let locator = new ServiceLocator();
  locator.loadService("FingerPrintSensor".new FingerPrintSensor());
  locator.loadService("PasswordKeyboard".new PasswordKeyboard());
  locator.loadService("Camera".new Camera());
  locator.loadService("RFIDReader".newRFIDReader()); ServiceLocator.load(locator); }) ();Copy the code

The above code allows developers to build classes based on certain configurations; In addition, it provides a new way to automate tests. (TDD)

IoC Container

An IoC container refers to a design Framework such as Inversify and the well-known NestJS. A typical use would be to decorate a Service with the @Injectable () period, declare in the controller class that the previous instance of the Service needs to be injected during class construction, and register the Service with the Nest IoC container.

  1. Dependency Injection (declare classes to be injected)
// cat.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable(a)export class CatsService {
  private readonly cats: Cat[] = [];

  findAll(): Cat[] {
    return this.cats; }}Copy the code
  1. The injected class
// cats.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get(a)async findAll(): Promise<Cat[]> {
    return this.catsService.findAll(); }}Copy the code
  1. Vessel registration
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}
Copy the code

reflection

WiKi explanation of reflection

Reflection is the ability of a computer program to access, detect, and modify its own state or behavior at runtime. Metaphorically, reflection is the ability of a program to “observe” and modify its behavior as it runs.

The reflection approach is basically used in layered development patterns, and IoC can be seen as a layered development pattern. Using reflection makes your program more elegant and neat. In JS, three static methods related to reflection are mainly used, Object, Reflect and Proxy, to extend and modify the class or instance being operated.

Code case

To illustrate, I’ll use TypeScript as an example:

import "reflect-metadata";

const GUARDER = Symbol("Guarder");
type Constructor<T = any> = new(... args:any[]) => T;
const __KindOfGuarder: Function[] = [];
const Injectable = (): ClassDecorator= > (target) = > {};

const KindOfGuarder = (name): ClassDecorator= > {
  return (target) = > {
    __KindOfGuarder.push(target);
    Reflect.defineMetadata(GUARDER, name, target);
  };
};
class Guarder {
  getKeyInfo() {
    throw "Implement validation input"; }}@KindOfGuarder("finger")
class FingerPrintSensor extends Guarder {
  readFingerPrint() {
    console.log("Read fingerprint");
  }
  getKeyInfo = this.readFingerPrint;
}

@KindOfGuarder("password")
class PasswordKeyboard extends Guarder {
  readInput() {
    console.log("Read password");
  }
  getKeyInfo = this.readInput;
}
@Injectable(a)class Guard {
  constructor(public readonly guarder: Guarder) {}
  verifyMethod() {
    this.guarder.getKeyInfo(); }}const Factory = <T>(target: Constructor<T>, name): any= > {
  const providers = Reflect.getMetadata("design:paramtypes", target);
  const index = __KindOfGuarder.findIndex(
    (guarder) = > Reflect.getMetadata(GUARDER, guarder) === name
  );
  if (index >= 0) {
    let instance = new (<Constructor>__KindOfGuarder[index])();
    if (
      providers &&
      providers.length === 1 &&
      instance instanceof providers[0]) {return newtarget(instance); }}else {
    throw "No type found to construct."; }}; Factory(Guard,"finger").verifyMethod();
Factory(Guard, "password").verifyMethod();
Copy the code

Using decorators (lines 20 and 28), register the extended classes one by one (lines 10). (Lines 61 and 62) Use the factory pattern to combine the calling aspect and being called, and execute the associated method.

conclusion

IoC and DIP are both SOLID programming principles first proposed by Uncle Robert C. Martin. The goal is to remove strong coupling between programs to accommodate extensible and modifiable maintenance requirements. There is a growing trend for front-end projects (and possibly Node back-end), but when it comes to programming with OOP and in the face of large-scale projects, both IoC and DIP are incircumstancable skill trees that need to be firmly mastered.

With the Reflect object in JS, and the plug-in of reflect-metadata and TypeScript, which is a strong superset of types, JS programming becomes more and more faceted and large-scale. It’s not the language that was designed in 10 days to do simple things on the front end of a web page.

If there is something wrong, thank you very much and welcome to point it out.