一个应用程序实际就是非常多的抽象类,通过互相调用实现应用的所有功能。随着应用代码和功能复杂度的增加,项目一定会越来越难以维护,因为类越来越多,相互之间的关系越来越复杂。举个例子,假如我们使用 Koa 开发我们的应用,Koa 本身主要实现了一套基础的 Web 服务能力,我们在实现应用的过程中,会定义很多类,这些类的实例化方式、相互依赖关系,都会由我们在代码逻辑自由组织和控制。每个类的实例化都是由我们手动 new,并且我们可以控制某个类是只实例化一次然后被共享,还是每次都实例化。下面的 B 类依赖 A,每次实例化 B 的时候,A 都会被实例化一次,所以对于每个实例 B 来说,A 是不被共享的实例。class A{}
// B
class B{
contructor(){
this.a = new A();
}
}下面的 C 是获取的外部实例,所以多个 C 实例是共享的 app.a 这个实例。class A{}
// C
const app = {};
app.a = new A();
class C{
contructor(){
this.a = app.a;
}
}下面的 D 是通过构造函数参数传入,可以每次传入一个非共享实例,也可以传入共享的 app.a 这个实例(D 和 F 共享 app.a),并且由于现在是参数的方式传入,我也可以传入一个 X 类实例。class A{}
class X{}
// D
const app = {};
app.a = new A();
class D{
contructor(a){
this.a = a;
}
}
class F{
contructor(a){
this.a = a;
}
}
new D(app.a)
new F(app.a)
new D(new X())这种方式就是依赖注入,把 B 所依赖的 A,通过传值的方式注入到 B 中。通过构造函数注入(传值)只是一种实现方式,也可以通过实现 set 方法调用传入,或者是其他任何方式,只要能把外部的一个依赖,传入到内部就行。其实就这么简单。class A{}
// D
class D{
setDep(a){
this.a = a;
}
}
const d = new D()
d.setDep(new A())
3.2 All in 依赖注入?
随着迭代进行,出现了 B 根据不同的前置条件依赖会发生变化。比如,前置条件一 this.a 需要传入 A 的实例,前置条件二this.a需要传入 X 的实例。这个时候,我们就会开始做实际的抽象了。我们就会改造成上面 D 这样依赖注入的方式。初期,我们在实现应用的时候,在满足当时需求的情况下,就会实现出 B 和 C 类的写法,这本身也没有什么问题,项目迭代了几年之后,都不一定会动这部分代码。我们要是去考虑后期扩展什么的,是会影响开发效率的,而且不一定派的上用场。所以大部分时候,我们都是遇到需要抽象的场景,再对部分代码做抽象改造。// 改造前
class B{
contructor(){
this.a = new A();
}
}
new B()// 改造后
class D{
contructor(a){
this.a = a;
}
}
new D(new A())
new D(new X())
按照目前的开发模式,CBD三种类都会存在,B 和 C有一定的几率发展成为 D,每次升级 D 的抽象过程,我们会需要重构代码,这是一种实现成本。这里举这个例子是想说明,在一个没有任何约束或者规定的开发模式下。我们是可以自由的写代码来达到各种类与类之间依赖控制。在一个完全开放的环境里,是非常自由的,这是一个刀耕火种的原始时代。由于没有一个固定的代码开发模式,没有一个最高行动纲领,随着不同开发人员的介入或者说同一个开发者不同时间段写代码的差别,代码在增长的过程中,依赖关系会变得非常不清晰,该共享的实例可能被多次实例化,浪费内存。从代码中,很难看清楚一个完整的依赖关系结构,代码可能会变得非常难以维护。那我们每定义一个类,都按照依赖注入的方式来写,都写成 D 这样的,那 C 和 B 的抽象过程就被提前了,这样后期扩展也比较方便,减少了改造成本。所以把这叫All in 依赖注入,也就是我们所有依赖都通过依赖注入的方式实现。可这样前期的实现成本又变高了,很难在团队协作中达到统一并且坚持下去,最终可能会落地失败,这也可以被定义为是一种过度设计,因为额外的实现成本,不一定能带来收益。
Nestjs实现了控制反转,约定配置模块(@module)的 imports、exports、providers 管理提供者也就是类的依赖注入。providers 可以理解是在当前模块注册和实例化类,下面的 A 和 B 就在当前模块被实例化,如果B在构造函数中引用 A,就是引用的当前 ModuleD 的 A 实例。import { Module } from ‘@nestjs/common’;
import { ModuleX } from ‘./moduleX’;
import { A } from ‘./A’;
import { B } from ‘./B’;@Module({
imports: [ModuleX],
providers: [A,B],
exports: [A]
})
export class ModuleD {}
// B
class B{
constructor(a:A){
this.a = a;
}
}
exports 就是把当前模块中的 providers 中实例化的类,作为可被外部模块共享的类。比如现在 ModuleF 的 C 类实例化的时候,想直接注入 ModuleD 的 A 类实例。就在 ModuleD 中设置导出(exports)A,在 ModuleF 中通过 imports 导入 ModuleD。按照下面的写法,控制反转程序会自动扫描依赖,首先看自己模块的 providers 中,有没有提供者 A,如果没有就去寻找导入的 ModuleD 中是否有 A 实例,发现存在,就取得 ModuleD 的 A 实例注入到 C 实例之中。import { Module } from ‘@nestjs/common’;
import { ModuleD} from ‘./moduleD’;
import { C } from ‘./C’;@Module({
imports: [ModuleD],
providers: [C],
})
export class ModuleF {}