(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 依赖注入使用,并通过部门服务例子进行演示。

参考资料

相关推荐
吃饺子不吃馅3 分钟前
【八股汇总,背就完事】这一次再也不怕webpack面试了
前端·面试·webpack
Amos_Web10 分钟前
Rust实战教程--文件管理命令行工具
前端·rust·全栈
li理19 分钟前
鸿蒙相机开发入门篇(官方实践版)
前端
webxin66620 分钟前
页面动画和延迟加载动画的实现
前端·javascript
逛逛GitHub32 分钟前
这个牛逼的股票市场平台,在 GitHub 上开源了。
前端·github
Max81233 分钟前
Agno Agent 服务端文件上传处理机制
后端
调试人生的显微镜40 分钟前
苹果 App 怎么上架?从开发到发布的完整流程与使用 开心上架 跨平台上传
后端
细节控菜鸡1 小时前
【排查实录】Web 页面能打开,服务器能通接口,客户端却访问失败?原因全在这!
运维·服务器·前端
顾漂亮1 小时前
Spring AOP 实战案例+避坑指南
java·后端·spring
duandashuaige1 小时前
解决用electron打包Vue工程(Vite)报错electron : Failed to load URL : xxx... with error : ERR _CONNECTION_REFUSED
javascript·typescript·electron·npm·vue·html