(3)NestJS 依赖注入DI与控制反转IOC

1. 前言

在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Controller 中。

2. 概念

2.1 依赖注入、控制反转、容器

何为容器?容器是可以装一些资源的。举个例子,普通的容器,生活中的容器比如保温杯,只能用来存储东西,没有更多的功能。而程序中的容器则包括数组、集合Set、Map 等。

而复杂的容器,生活中比如政府,政府管理我们的一生,生老病死和政府息息相关。

程序中复杂的容器比如NestJS 容器,它能够管理 Controller、Service 等组件,负责创建组件的对象、存储 组件的对象,还要负责 调用 组件的方法让其工作,并在一定的情况下 销毁 组件。

依赖注入(Dependency Injection)是实现控制反转的一种方式 。控制反转又是什么呢?控制反转(Inversion of Control)是指从容器中获取资源的方式跟以往有所不同。

2.2 为什么需要控制反转

2.2.1 依赖关系复杂、依赖顺序约束

后端系统中有多个对象:

  • Controller 对象: 处理 HTTP 请求,调用 Service,返回响应。
  • Service 对象: 实现业务逻辑。
  • Repository 对象: 实现对数据库的增删改查。

此外,还包括数据库链接对象 DataSource、配置对象 Config 等等。这些对象之间存在复杂的关系:

  • Controller 依赖 Service 实现业务逻辑。
  • Service 依赖 Repository 进行数据库操作。
  • Repository 依赖 DataSource 建立连接,而 DataSource 则需要从 Config 对象获取用户名密码等信息。

这导致对象的创建变得复杂,需要理清它们之间的依赖关系,确保正确的创建顺序。例如:

js 复制代码
const config = new Config({ username: 'xxx', password: 'xxx'});
const dataSource = new DataSource(config);
const repository = new Repository(dataSource);
const service = new Service(repository);
const controller = new Controller(service);

这些对象需要一系列初始化步骤后才能使用。此外,像 config、dataSource、repository、service、controller 这些对象不需要每次都新建一个,可以保持单例。在应用初始化时,需要明确依赖关系,创建对象组合,并确保单例模式,这是后端系统常见的挑战。

2.3.2 高层逻辑直接依赖低层逻辑,违反依赖倒置规范

依赖倒置: 什么是依赖倒置原则(Dependency Inversion Principle)高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。 抽象不应该依赖细节,细节(具体实现)应该依赖抽象。

1.举一个工厂例子,初始化时有工人、车间、工厂。

1.工厂是容器,车间是消费者,依赖工人和工人的服务,工人是依赖,是生产者。

js 复制代码
// 工人
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()

2.现在要把工人制造改为机器制造,需要直接在车间把工人制造改为机器制造,麻烦。

js 复制代码
// 机器
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()

3.此时考虑依赖倒置原则,通过实现生产者接口来处理(PS:这也是为什么像 Java 语言中,实现业务服务时需要定义接口类和实现类,遵循依赖倒置,方便切换不同的业务逻辑)

js 复制代码
// 定义一个生产者接口
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()

    

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

要完全遵守依赖倒置原则,需要使用控制反转依赖注入

2.3 控制反转思想

2.3.1 获取资源的传统方式

  • 自己做饭:买菜、洗菜、择菜、切菜、炒菜,全过程参与,费时费力,必须了解资源创建整个过程中的全部细节并熟练掌握。
  • 在应用程序中,组件需要获取资源,传统的方式是主动从容器中获取所需资源,这样开发人员需要知道具体容器中特定资源的获取方式,增加了学习成本,也降低了开发效率。

2.3.2 获取资源的控制反转方式

  • 点外卖:下单、等待、吃外卖,省时省力,不必关心资源创建过程的全部细节。
  • 控制反转的思想改变了应用程序组件获取资源的方式,容器会主动将资源推送给需要的组件,开发人员只需要提供接收资源的方式即可,这样可以降低学习成本,提高开发效率。这种方式被称为查找的被动方式。

2.4 如何实现控制反转

起源:许多应用程序的业务逻辑实现需要两个或多个类之间的协作,这种协作使得每个对象都需要获取与其合作的对象(即其所依赖的对象的引用)。如果这种获取过程由对象自身实现,那么将导致代码高度耦合,难以维护和调试。

技术描述

在 Class A 中,我们使用了 Class B 的对象 b。通常情况下,我们需要在 A 的代码中显式地使用 new 来创建 B 的对象。但是,如果采用依赖注入技术,A 的代码只需要定义一个 private 的 B 对象,而不需要直接 new 来获取这个对象。相反,我们可以通过相关的容器控制程序来在外部创建 B 对象,并将其注入到 A 类中的引用中。具体获取的方法以及对象被获取时的状态由配置文件(如 XML)来指定。这种方法可以使代码更加清晰和正式。

loc 也可以理解为把流程的控制从应用程序转移到框架之中 。以前,应用程序掌握整个处理流程;现在,控制权 转移到了框架,框架利用一个引擎驱动整个流程的执行,框架会以相应的形式提供一系列的扩展点,应用程序则通过定义扩展的方式实现对流程某个环节的定制。"框架Call 应用"。基于 MVC 的 web 应用程序就是如此。

实现方法

实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类 A 的实例创建过程中即创建了依赖的 B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的事件也可以在代码中自由控制。

细说

1.依赖注入:

  • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象
  • 基于set方法。实现特定属性的publicSet方法,来让外部容器调用传入所依赖类型的对象
  • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象
  • 基于注解。基于Java的注解功能,在私有变量前加"@Autowired"等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。

2.依赖查找

依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。

2.4.1 工厂例子依赖注入改造

通过以上学习,现在把工厂例子代码进一步改造,将底层类的依赖,由从中间类直接引用,变为高层类在构造时的依赖注入

js 复制代码
// ......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()

至此,回顾对这个车间的改造三步

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

3. NestJS 依赖注入

在Nest的设计中遵守了控制反转的思想,使用依赖注入(包括构造函数注入、参数注入、Setter方法注入)解藕了Controller 与 Provider之间的依赖。

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

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

IOC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。

Nest 实现了 IOC 容器,会从入口模块开始扫描,分析 Module 之间的引用关系,对象之间的依赖关系,自动把 provider 注入到目标对象。

Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。并且Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。

provider 一般都是用 @Injectable 修饰的 class:

在 Module 的 providers 里声明:

上面是一种简写,完整的写法是这样的

构造函数或者属性注入

异步的注入对象

通常情况下,提供者通过使用 @Injectable 声明,然后在 @Moduleproviders 数组中注册类来实现。默认的 token 是类本身,因此不需要使用 @Inject 来指定注入的 token。

但是,也可以使用字符串类型的 token,但在注入时需要单独指定 @Inject。除了可以使用 useClass 指定注入的类,还可以使用 useValue 直接指定注入的对象。如果想要动态生成对象,则可以使用 useFactory,它的参数也注入到 IOC 容器中的对象中,然后动态返回提供者的对象。如果想要为已有的 token 指定一个新的 token,可以使用 useExisting 来创建别名。通过灵活运用这些提供者类型,可以在 Nest 的 IOC 容器中注入任何对象。

4.实践

之前部门逻辑都是放在 controller 中的,现在可以把逻辑放 dept.service.ts 上来,添加 @Injectable 装饰器。

在 DeptModule 模块中的 propviders 中引入 DeptService

最后在 dep.controller 使用部门服务,通过 @Inject() 装饰器注入。

小结

本文我们学习了什么是容器、依赖注入与控制反转,掌握了 Nest 依赖注入使用,并通过部门服务例子进行演示。

参考资料

相关推荐
m0_748235612 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
超爱吃士力架9 小时前
邀请逻辑
java·linux·后端
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式