JavaScript 的 IoC、IoC Containers、DI、DIP和Reflect

2,651 阅读8分钟

基本概念

来自WiKi对IoC的解释

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递(注入)给它。

简单的概括一下来说:传统情况下,开发者写程序调用框架(其他程序员写的);而IoC则是让框架调用开发者的代码。IoC是一种法则(或称之为原则),我的理解是它并不属于某一种开发模式(Pattern),因为它是一个比较普世的概念。然而,开发模式则是某个法则的实现(或者说最佳实践)。例如,依赖注入(Dependency Injection)就是一种IoC的实现,工厂模式(Factory,或者抽象工厂)也是一种实现。

另外IoC的一大应用领域是TDD(测试驱动开发),这是基于IoC理念之上的一种测试方法。

来自WiKi对DIP对解释

依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

依赖反转原则是SOLID的原则之一(其中的D)。其解偶强调高层类(调用方)不依赖于低层(low-level)类(被调用方)。

案例代码

比如:大楼门禁系统有指纹锁、密码锁、刷脸锁和磁卡锁,一个门禁系统包含、一扇门,门上有一把锁,一个验证机构。比较传统的做法(强耦合),将会如下代码:

// 锁机构
class Locker {}
// 房门
class Door {}
// 指纹传感器
class FingerPrintSensor {
  readFingerPrint() {}
}
// 键盘
class PasswordKeyboard {
  readInput() {}
}
// 摄像头
class Camera {
  takePicture() {}
}
// 读卡器
class RFIDReader {
  readCard() {}
}

// 指纹门禁
export class FingerPrintGuard {
  fingerPrintSensor = new FingerPrintSensor();
  locker = new Locker();
  docker = new Door();
}
// 密码门禁
export class PasswordGuard {
  keyboard = new PasswordKeyboard();
  locker = new Locker();
  docker = new Door();
}
// 刷脸门禁
export class FacialGurad {
  camera = new Camera();
  locker = new Locker();
  docker = new Door();
}
// 磁卡门禁
export class IDGuard {
  reader = new RFIDReader();
  locker = new Locker();
  docker = new Door();
}

利用DIP原则重构代码,那么我们就必须将低层类FingerPrintSensorPasswordKeyboardCameraRFIDReader,抽象成一个抽象类,Guarder。(因JS不支持abstract关键词,用普通类加上抛异常来编写代码),解除高层类*Guard对其的依赖。

export class Guarder {
  getKeyInfo() {
    throw "实现验证输入";
  }
}

再将上述四个类作扩展这个抽象类

// 指纹传感器
class FingerPrintSensor extends Guarder {
  readFingerPrint() {}
  getKeyInfo = this.readFingerPrint;
}
// 键盘
class PasswordKeyboard extends Guarder {
  readInput() {}
  getKeyInfo = this.readInput;
}
// 摄像头
class Camera extends Guarder {
  takePicture() {}
  getKeyInfo = this.takePicture;
}
// 读卡器
class RFIDReader extends Guarder {
  readCard() {}
  getKeyInfo = this.readCard;
}

此时,高层门禁类会改为如下:

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

因为JS是弱类型语言,如果用TS或者其他高级语言则会更容易理解;通过上述代码不难发现,我们已经将门禁和门禁特性拆开了。同理,以IoC原则的依赖注入(DI)方式来理解,那么少许改动Guard(typescript)

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

两者差别

目的是为了解偶而产生的设计原则是IoC和DIP相同点,两者区别是:

  • IoC从程序流控制方面切入,解除依赖于被依赖的关系
  • DIP从对类的设计上面切入,处理调用与被调用对象的依赖关系,增加一个中间部分(抽象类)

两者本质上不矛盾,大部分内容是相同的。

IoC的实现模式(pattern)

用DI的方式实现IoC

依赖注入(dependency injection)的意思为,给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。

上面的例子已经简单的利用了构造函数对被依赖的对象(类)进行了注入操作,还可以利用一些其他的方式进行注入操作,比如专门使用一个绑定函数。还可以利用现在比较流行且优雅的方式装饰器来注入,有兴趣的同学可以参考此前我写的一篇文章

class Guard {
  @Guarder // 属性装饰器
  guarder = {};
  locker = new Locker();
  docker = new Door();
}

利用这种开发模式,底层的框架可以设计出诸多装饰器,而具体的逻辑则由调用方面的代码处理。

其他的方式

服务定位模式(SL),在本是弱类型的JS中用的比较少(但是Java或其他高级语言,特别是大型框架中相对较多)。其实现原理大致上是将依赖的类装入到一个词典中(或Map),然后利用一个配置文件或者常量来在需要调用(依赖)时候利用服务加载器来实现引用过程。以此来实现一定程度解偶。我们还是拿门禁的案例来说明:

// 服务定位器(类)
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]);
    return ServiceLocator.sInstance.services[key];
  }
}

// 门禁,可以根据配置加载
class Guard {
  guarder = ServiceLocator.getService("Camera");
  locker = new Locker();
  docker = new Door();
}

// 注册服务
(function () {
  let locator = new ServiceLocator();
  locator.loadService("FingerPrintSensor", new FingerPrintSensor());
  locator.loadService("PasswordKeyboard", new PasswordKeyboard());
  locator.loadService("Camera", new Camera());
  locator.loadService("RFIDReader", new RFIDReader());
  ServiceLocator.load(locator);
})();

以上代码,让开发者可以依据某种配置来构建类;另外,从自动化测试方面来讲,提供了一种新的方法。(TDD)

IoC Container

IoC容器,指的是一种设计框架(Framework),例如inversify和知名的NestJS。其中一个典型的用法,就是用@Injectable装时期修饰一个Service,然后在控制器类中声明其先前的Service实例需要在类构造时注入其中,最后用Nest IoC容器注册这个服务。

  1. 依赖注入(声明需要注入的类)
// cat.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

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

  findAll(): Cat[] {
    return this.cats;
  }
}
  1. 被注入的类
// 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()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
  1. 容器注册
// 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 {}

反射

来自WiKi对反射的解释

反射(英语:reflection)是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

分层的开发模式基本上都会用到反射方法,IoC可以看作为是一种分层的开发模式。利用反射,会让程序更为优雅和整洁。JS中,关于反射主要用到三个对象的相关静态方法,Object、Reflect和Proxy,对被操作的类或者实例进行扩展、修改操作。

代码案例

为了方便演示,我利用TypeScript来举例:

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 "实现验证输入";
  }
}

@KindOfGuarder("finger")
class FingerPrintSensor extends Guarder {
  readFingerPrint() {
    console.log("读取指纹");
  }
  getKeyInfo = this.readFingerPrint;
}

@KindOfGuarder("password")
class PasswordKeyboard extends Guarder {
  readInput() {
    console.log("读取密码");
  }
  getKeyInfo = this.readInput;
}
@Injectable()
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 new target(instance);
    }
  } else {
    throw "没有找到可以构造的类型";
  }
};

Factory(Guard, "finger").verifyMethod();
Factory(Guard, "password").verifyMethod();

(20和28行)先利用装饰器,(10行)将扩展类一个个注册。(61和62行)使用工厂模式,将调用方面和被调用组合起来,并执行相关方法。

总结

IoC、DIP都是最先由Robert C. Martin大叔提出的,围绕其提出的SOLID编程原则。目的是解除程序之间强耦合性,以适应可扩展和可修改的维护需求。前端项目(也可能是Node后端)有着越来越大的发展趋势,但凡使用OOP的编程(思想)并在大规模项目面前,IoC和DIP都是绕不过去的技能树,非常有必要牢固地掌握。

配合JS中的Reflect对象,再外挂上reflect-metadata以及TypeScript这个强类型超集,使得JS的编程越来越面相与规模化。已经完全不是那个仅用了10天时间被设计出来用在网页前端做一些简单操作的那个语言。

如果有不对的地方,非常感谢和欢迎同学们指出。