likes
comments
collection
share

Nest.js入门 —— 控制反转与依赖注入(一)

作者站长头像
站长
· 阅读数 12

Nest.js入门系列文章链接:

随着Node.js的出现,JavaScript一举成为了一个前后端通用的语言。不过,与前端领域中借助Node.js出现了一批优秀的工程化框架如Angular、React、Vue等不同,在后端领域出现的Express、Koa等著名工具都没有能够解决一个重要的问题——架构。Nest正是在这样的背景下出现的,它深受Angular设计思想的启发,而Angular 的很多模式又来自于 Java 中的 Spring 框架,所以我们可以说Nest就是 Node.js版的 Spring 框架。

因此对于很多Java后端同学来说,Nest中的设计与其编写方式都是非常容易理解的,但是对于前端出身的传统JS程序员,仅仅提到Nest中最主要最核心的思想如控制反转、依赖注入等概念就让人望而却步,更别说其原理还涉及到了TypeScript、装饰器、元数据、反射等等相关概念,再加上其官方文档及核心社区都是英文,使得许多同学都被挡在了门外。

Nest.js入门系列文章将从Nest的设计思想出发详细讲解其相关概念及原理,最终模仿实现一个极其简易(也可以说是简陋)的FakeNest框架。一方面让已经使用并希望进一步了解Nest原理的同学能够有所收获,另一方面也力图让从事传统JS前端开发的同学能够入门并了解借鉴到后端开发中的一些优秀思想。

本文为Nest.js入门的第一篇,将详细讲述在Nest的实现中最核心的设计思想——控制反转(IOC)与依赖注入(DI)。我们将通过一个制造工厂类改造的示例来一步步为你揭开它们的面纱。

一、制造工厂

首先,让我们看看一个最基础的制造工厂类应该是什么样的。

// 工人
class Worker {
  manualProduceScrew(){
    console.log('A screw is built')
  }
}

// 螺丝生产车间
class ScrewWorkshop {
  private worker: Worker = new Worker()
  
  produce(){
    this.worker.manualProduceScrew()
  }
}

// 工厂
class Factory {
  start(){
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()

在这个简化版的工厂中,我们仅设计了三个最基础的类来负责螺丝的制造工作。乍看上去这个设计并没有什么问题,当工程希望生产螺丝时,工厂直接向螺丝生产车间下达了生产指令,而螺丝生产车间进一步向工人下达了生产指令,最终螺丝被生产了出来。

然而过了一段时间后,工厂新进了一批自动化螺丝生产设备,厂长希望使用这批设备代替工人的工作从而降低生产成本,于是,我们需要对这个汽车工厂的代码进行改造!

// 机器
class Machine {
  autoProduceScrew(){
    console.log('A screw is built')
  }
}

class ScrewWorkshop {
  // 改为一个机器实例
  private machine: Machine = new Machine()
  
  produce(){
    this.machine.autoProduceScrew()
  }
}

class Factory {
  start(){
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()

在费了一番力气改造了螺丝生产车间后,螺丝又一次被生产制造了出来。但是,没过多久,厂长就发现并购入了一批价格更低、生产效率更高的机器,于是我们又一次需要改造螺丝生产车间。试想如果这样的情况不断发生,我们就需要不断花费力气改造螺丝生产车间,很快生产车间的主任将会对我们不耐烦起来。那我们有没有可能在不改变螺丝生产车间的情况下就能够替换其底层的生产方式呢?

二、工厂改造

首先,我们分析一下上面这个符合直觉的设计到底存在什么问题。Machine/Worker类是最终执行生产动作的类,他们都归属于螺丝生产车间ScrewWorkshop这个类。从这个方面来讲,Machine/Worker类应该是低层类,而ScrewWorkshop应该为高层类,工厂中的高层类依赖了低层类。于是,我们的错误就呼之欲出了,那就是这个工厂的设计违背了依赖倒置原则。

什么是依赖倒置原则(Dependency Inversion Principle)

  1. High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).

    高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。

  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

    抽象不应该依赖细节,细节(具体实现)应该依赖抽象。

因此,我们首先应该让Machine/Worker类以及ScrewWorkshop类解藕,让螺丝生产车间为其所希望使用的低层生产方式类定义一个接口,让Machine/Worker等低层类遵循并实现这个接口。

// 定义一个生产者接口
interface Producer {
  produceScrew: () => void
}

// 实现了接口的机器
class Machine implements Producer {
  autoProduceScrew(){
    console.log('A screw is built')
  }
  
  produceScrew(){
    this.autoProduceScrew()
  }
}

// 实现了接口的工人
class Worker implements Producer {
  manualProduceScrew(){
    console.log('A screw is built')
  }
  
  produceScrew(){
    this.manualProduceScrew()
  }
}

class ScrewWorkshop {
  // 依赖生产者接口,可以随意切换啦!!!
  // private producer: Producer = new Machine()
  private producer: Producer = new Worker()
  
  produce(){
    this.producer.produceScrew()
  }
}

class Factory {
  start(){
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()

在经过对工厂这样的一番改造后,今后螺丝生产车间改造的工作明显变得更加轻松了,只需要改变其属性中所新建的遵循Producer接口的实例即可。然而,这并没有完全改善我们与车间主任之间的关系,每次厂里在改造生产机器时我们还是需要麻烦车间主任。这是因为我们还是没有能够完全遵守依赖倒置原则,ScrewWorkshop仍然依赖了Worker/Machine的实例,只不过这种依赖相较之前少了一点罢了。

那么如何能够完全遵守这个依赖倒置原则从而摆脱这项任务呢?这时就轮到控制反转与依赖注入来帮忙啦!

什么是控制反转(Inversion Of Control)

控制反转是一种设计原则。顾名思义,它用于在面向对象设计中反转不同种类的控制以实现松耦合。在这里,控制是指一个类中除了完成其主要工作流程之外的其他所有流程,包括对应用程序流程的控制,以及对依赖对象创建和绑定流程的控制。

什么是依赖注入(Dependency Injection)

控制反转只告诉了我们需要怎么去做,但并没有告诉我们应该怎么做。所以实现控制反转的手段多种多样,其中比较流行的也是Nest、Spring等主流框架所使用的手段就是依赖注入。

依赖注入允许在类之外创建依赖对象,并通过不同的方式将这些对象提供给类。使用依赖注入的手段,我们能够将类所依赖对象的创建和绑定移动到类自身的实现之外。

不同的方式包括:构造函数注入、属性注入、Setter方法注入、接口注入。

我不想看概念了,能简单的说一下它们到底做了什么吗?

通俗的说通过控制反转和依赖注入实现了以下功能:

如果类A需要类B,类A中并不直接控制创建类B的实例。与之相反,我们从类A外部控制类B实例的创建,类A之中只负责使用类B的实例,完全无需关心类B实例是如何创建的。

下面,我们将使用它们来对我们的工厂进行进一步的改造。

// ......Worker/Machine及其所遵循的接口Producer的实现与此前一致,此处省略

class ScrewWorkshop {
  private producer: Producer
  
  // 通过构造函数注入
  constructor(producer: Producer){
    this.producer = producer
  }
  
  produce(){
    this.producer.produceScrew()
  }
}

class Factory {
  start(){
    // 在Factory类中控制producer的实现,控制反转啦!!!
    // const producer: Producer = new Worker()
    const producer: Producer = new Machine()
    // 通过构造函数注入
    const screwWorkshop = new ScrewWorkshop(producer)
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()

终于,我们不用再麻烦车间主任了,他也可以自由下班去打牌喝酒了!

三、Nest中的”工厂“

Nest.js入门 —— 控制反转与依赖注入(一)

我们再来通俗的解释一下(并不是精准的对应,只是为了让读者能够感受到侧重点),在对这个车间的改造过程中我们都做了些什么:

  1. 依赖倒置: 解除ScrewWorkshop与Worker/Machine具体类之间的依赖关系,转为全部依赖Producer接口;
  2. 控制反转: 在Factory类中实例化ScrewWorkshop中需要使用的producer,ScrewWorkshop的对依赖项Worker/Machine的控制被反转了;
  3. 依赖注入: ScrewWorkshop中不关注具体producer实例的创建,而是通过构造函数constructor注入;

需要明确的是依赖倒置和控制反转都是设计原则,只是一种思想,而依赖注入才是是真正的实现手段。在Nest的设计中遵守了控制反转的思想,使用依赖注入(包括构造函数注入、参数注入、Setter方法注入)解藕了Controller与Provider之间的依赖。

最后,我们将Nest中的元素与我们自己编写的工厂进行一个类比:

  1. Provider & Worker/Machine:真正提供具体功能实现的低层类。
  2. Controller & ScrewWorkshop:调用低层类来为用户提供服务的高层类。
  3. Nest框架本身 & Factory:控制反转容器,对高层类和低层类统一管理,控制相关类的新建与注入,解藕了类之间的依赖。

四、总结

依赖注入和控制反转的思想在后端领域已经撑的上是必学必懂的基础了,然而在前端领域却一直不温不火。但是通过对这种思想的学习了解是非常有助于前端同学培养面向对象编程的思维,从而能够写出低耦合高内聚的良好代码。

回到本系列的主要任务,学习了解依赖注入和控制反转思想是理解Nest.js源码的基础。通过结合控制反转、依赖注入的思想以及装饰器、元数据的使用,Nest.js的核心设计就能够轻松的完成。下一篇中,我们将学习什么是装饰器、什么是元数据,以及它们的结合又会带来怎样的效果。