基本概念
来自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原则重构代码,那么我们就必须将低层类FingerPrintSensor
,PasswordKeyboard
,Camera
,RFIDReader
,抽象成一个抽象类,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容器注册这个服务。
- 依赖注入(声明需要注入的类)
// 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;
}
}
- 被注入的类
// 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();
}
}
- 容器注册
// 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天时间被设计出来用在网页前端做一些简单操作的那个语言。
如果有不对的地方,非常感谢和欢迎同学们指出。