【Nodejs】理解Nestjs的IoC与DI
什么是IOC
Interversion Of Control
(控制反转)是面向对象编程中的一种设计原则:对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用注入给它。
设计IOC的目的:
- 把执行的任务和实现(
implement
)解耦。 - 使得开发者关注模块的任务设计上。
- 将模块从系统运行中抽离出来,取而代之的是契约关系。
- 防止更换模块产生副作用。
我们可以写一下伪代码,假设我们有一个Car
类,需要让一辆车跑起来。
一般是这么写:
Class Wheel {
constructor(size: number) {
this.size = size
}
}
class Car extends Wheel {
constructor(size) {
super(size)
}
run() {
console.log(this.size);
}
}
new Car(19).run();
很明显,Car 依赖了 Wheel 类,并且把 Wheel 和 Car 强耦合了在一起,在编写的时候更多关注与 Car 与 Wheel 类的整体系统。而忽略了拆分,在做单测的时候你不得不expect (new Car(4).size).toBe(4)
这样去书写。
用IOC解耦之后:
Class Wheel {
constructor(size) {
this.size = size
}
}
class Container {
constructor() { this.modules = {} }
provide(key, object) { this.modules[key] = object }
get(key) { return this.modules[key] }
}
const carModule = new Container();
carModule.provide('wheel', new Wheel(4, carModule))
class Car {
constructor(container: Container) {
this.wheel = container.get('wheel');
}
run() {
console.log(this.wheel)
}
}
new Car().run();
这样子经过一个IOC容器进行梳理,从Wheel为Car的依赖子类,变成 Wheel 和 Car 同级,这就是控制反转。
Nestjs中的IOC
Nestjs当中的Module就是IOC的体现。
- providers 依赖的service
- imports 依赖别的模块的service
- export 想要暴露的自身的service
- controllers 模块路由
里面的providers就像我上述所说的IOC container。更具体的体现可以看module reference文档。
什么是DI
ioc是目的,di是手段。ioc是指让生成类的方式由传统方式(new)反过来,既程序员不调用new,需要类的时候由框架注入(di),是同一件不同层面的解读。
IOC 就是把传统的new SomeObject(props)
操作,反转成用传入不同的对象为参数来实现。依赖注入DI是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。
在Nest中我们往往能看到这样的代码
import { AppService } from './app.service'
// app.controllers.ts
@Controller()
class AppControler {
constructor(appService: AppService) {}
@Get()
find(xx) {
return this.appService.find(xx);
}
}
AppController
依赖了AppService
,但是没有显性的在constructor中声明 this.appService = new AppService
,而是交给了IOC 容器,由IOC容器去获取AppService,在调用的AppController
的时候去帮我们自动注入,自动new AppService
。
接下来介绍一下如何实现一个依赖注入
Reflect Metadata
https://rbuckton.github.io/reflect-metadata/
https://www.typescriptlang.org/docs/handbook/decorators.html
基于Typescript实现一个DI(依赖注入)
在ts中使用Reflect Metadata
,由于是实验性api需要下载reflect-metadata
这个库。
npm i reflect-metadata --save
// ts.config
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
在ts中使用这个库的时候,当我们在 constructor 注入依赖项时,我们可以在ReflectMetada中拿到该类型。
第一步:实现一个Car 依赖注入一个Framework对象
import 'reflect-metadata'
type Constructor<T = any> = new (...args: any[]) => T;
class Framework {
public type = 'ev';
}
function Injectable() {
return (target) => {
console.log(target)
}
}
//
@Injectable()
class Car() {
constructor(public framework: Framework) {
}
}
function bootstrap<T>(target: Constructor<T>): T {
// todo
// assert car.framework.type to be ev
}
首先,在ts中,使用装饰器,它会在编译阶段自动的给我们注入metadata。上述代码编译过后是这样子的:
require("reflect-metadata");
class Framework {
constructor() {
this.type = 'ev';
}
}
function Injectable() {
return (target) => {
console.log(target);
};
}
let Car = class Car {
constructor(framework) {
this.framework = framework;
}
};
Car = __decorate([
Injectable(),
__metadata("design:paramtypes", [Framework])
], Car);
我们可以看到,这里在class constructor
中给我们自动注入了Framework
对象。我们可以很轻易的拿到
Reflect.getMetadata('design:paramtypes', Car) // 输出 [Framework]
沿着这个思路,我们只要调用new方法时,把constructor拿出所有依赖注入的对象,帮他new方法注入。
所以boostrap
可以这么写:
function bootstrap<T>(target: Constructor<T>): T {
const deps = Reflect.getMetadata('design:paramtypes', target);
return new target(...deps.map((ctor: Constructor<unknown>) => {
return new ctor();
}))
}
const car = bootstrap<Car>(Car)
结果:console.log(car.framework.type === 'ev') // true
第二步,如果依赖的对象也有依赖的对象,那么需要不断递归去bootstrap依赖的对象
在上述中,我们有了一个car对象,里面有framework属性,假设car有19寸轮毂,则framework里面会依赖一个Wheel,那么上述的bootstrap就会有问题了:
import 'reflect-metadata'
type Constructor<T = any> = new (...args: any[]) => T;
function Injectable() {
return (target) => {
}
}
@Injectable()
class Wheel {
public size = 19;
}
@Injectable()
class Framework {
public type = 'ev';
constructor(public wheel: Wheel) {
}
run() {
console.log(this.wheel.size);
}
}
//
@Injectable()
class Car {
constructor(public framework: Framework) {
}
}
// todo bootstrap
这里,我们可以使用不断递归循环的方式去获得,但是最好的方法是做一层container
,IOC
容器去管理。这样,可以有效的知道所有依赖项,可以对依赖项进行管理。
class Injector extends Map {
resolve<T>(target: Constructor<T>) {
const deps = Reflect.getMetadata('design:paramtypes', target) || [];
const depsInstance = deps.map((ctor: Constructor<unknown>) => {
return this.resolve(ctor);
})
const classInstance = this.get(target);
if (classInstance) {
return classInstance;
}
const newClassInstance = new target(...depsInstance);
this.set(target, newClassInstance)
return newClassInstance;
}
}
function bootstrap<T>(target: Constructor<T>): T {
const container = new Injector();
return container.resolve(target);
}
const car = bootstrap<Car>(Car)
console.log(car.framework.wheel.size === 19) // true
上述的Injector,我们还可以release所有依赖:
injector.clear() // 这样就可以把容器所有依赖给移除掉。
发表评论 (审核通过后显示评论):