大型边缘物联平台实战系列03-一文讲透依赖注入DI的前世今身及Nestjs面向接口编程

前情回顾

乡党们早上好,回顾上一篇人人都能用Nestjs开发标准的Restful接口,楼主着重给大家介绍了以下知识:

  1. 学会用ER关系图、用例图、类图来帮助分析业务(一图胜万语)
  2. 后端分层设计,控制层、服务层的划分
  3. Restful风格的HTTP接口

想必在座的各位应该都理解了吧,如果没有的话,是我的错,跟你们没关系…
在这里插入图片描述

没关系,这些知识点后面每一个模块我都会用到,因为这就是标准的概要设计流程。本节将进入一个小重点章节,注意哦,非常重要,尤其是对初次接触后端开发的同学来讲,很多后端开发通用的理论和概念将会出现:

  1. 为何要依赖注入?
  2. 什么样的代码是松耦合?
  3. 依赖关系(抽象or具体)
  4. UML聚合、依赖
  5. Nestjs依赖注入方式

楼主会从一个从未接触过后端开发人员的视角尽可能详细的讲解每一个知识点,整个边缘物联平台实战系列内容主要以Nestjs实战为主题,同时为了内容的完整性、真实性,你们还将额外学到以下知识,感兴趣的同学记得一定要点赞、收藏、订阅哦
在这里插入图片描述

问题分析

上一章节我们的工程申请接收者当接收到客户端发送的HTTP请求后,内部会动态实例化创建一个工程创建者,并调用它提供的create方法创建一个工程。

在这里插入图片描述

顺便科普一下,目前的代码就是UML类图的一种:依赖关系(表示A临时依赖B的关系,不长期。一般是局部变量、方法参数)(文末的码优集合章节会额外科普UML类图关系

在这里插入图片描述

从需求上,我们实现了功能,但是从设计上,这段代码隐患非常大…楼主本章节的重点就是通过需求的不断迭代,来逼迫我们不得已优化我们的代码,从中发现问题,最后带出依赖注入的前世今身

内存问题

假如有10000个客户端(浏览器、postman、curl命令、其他服务等)同时调用这个接口,短时间内会有10000个工程创建者出现,有一定几率造成大量的内存占用导致服务直接打出GG思密达(虽然V8引擎会垃圾回收)

在这里插入图片描述

肯定有一些同学要说了,初始化的时候创建一个工程创建者不就行了,也就是全局只有一个(单例),receive方法内只调用这个工程创建者的create方法即可。好的,你们说的对,这个思路是一点毛病没有,按照你们的思路我们改造一下:

在这里插入图片描述

这下你们满意了吧,目前的代码变成了UML类的聚合关系(表示A长期依赖B,一般是成员变量,长期持有,聚合关系也表示依赖,只是粘性更强)

在这里插入图片描述

很多同学一直不理解UML类的关系,网上大多数资料都是什么阿猫、阿狗之类的示例,这种很难与我们实际开发相结合,楼主的风格就是用实战带你理解这些概念,这样才有意义觉得楼主说的没问题的,记得评论区点赞

需求变更

做开发,要有良好的心态接受需求变更,楼主就是个典型代表,每次需求变更必定少不了一场battle…
在这里插入图片描述

产品经理提出新需求:

  1. 创建工程的弹出界面需要添加是否推送到云端的选型,如果用户选择了是,那么当工程创建成功后,我们系统需要调用云端接口把新工程的信息发送过去。
  2. 系统需要记录日志,从收到客户端请求到创建工程到消息推送,每一步都需要记录。防止后期出现扯皮现象,比如云端说我们没有推送啊BALALA的

直接甩了一张最新原型图、用例图给到我,让我自己消化~~,我消化你M。

在这里插入图片描述

在这里插入图片描述
哎,又让我装到了,连用例图怎么画都顺带交给大家了,必须点赞支持!

为了每个月两三个钢镚,也只能忍了,重新打开了vscode… 注意刚才新的需求,出现两个用例

  1. 推送消息
  2. 记录日志

在面向对象编程领域,所有的动词代表一个行为,该行为一定是某个类提供的能力。现在我们要看看,推送消息和记录日志,这两个能力应该由谁来提供。目前我们系统已经存在的类包含:

  1. ProjectApplyReceiver申请接收者
  2. ProjectCreator工程创建者
  3. Project工程实体

各位老铁,花一分钟从职责、内聚的角度思考一下,不难发现目前的类都不应该具备这个能力。我们又可以愉快的设计新类了:

  1. ProjectPusher消息推送员,提供推送消息的能力
  2. ProjectLogger日志记录员,提供记录日志的能力

在这里插入图片描述

打开vscode,根据之前文件的命名规则,我们创建 projectLogger.provider.ts 日志记录员

在这里插入图片描述

提供记录日志的能力:log方法内部会将 什么时间 完成了 什么事情 打印到控制台(暂时先到控制台,后期存到数据库或者文件)。这里注意,这个方法使用了异步async,因为后期我们要写文件或者存数据库都属于异步行为。async方法返回的类型必须是Promise, Promise<boolean>
括号内的boolean才是真正的数据类型,这种写法叫做泛型,强类型语言都有泛型的概念,各位也不必去专门学习泛型,看我文章就自动学会了,哎,又被我装到了…

继续创建 projectPusher.provider.ts 消息推送员,因为需求已经明确说了,每一步都需要记录日志。

那么我们的日志记录员应该在哪里创建?创建几个?
根据文章开头说的内存问题,我们创建一个日志记录员无疑是最合理的,其他角色把这个日志记录员通过构造函数参数引入作为成员变量即可,一旦作为成员变量,意味着长期持有,也就是聚合关系哦。嘿嘿,慢慢有一些依赖注入的影子了,
在这里插入图片描述

上图 projectPusher.provider.ts,通过构造函数注入了一个日志记录员,并且作为成员变量长期持有(UML类图的聚合关系)

现在我们的消息推送员已经可以借助日志记录员提供的记录日志方法来实现需求。而工程创建者也需要记录日志,同样也可以长期持有。照猫画虎修改下:

在这里插入图片描述

最后是我们的申请接收者,他同样需要记录日志,由于申请接收者是整个业务流程的第一站,由他来负责创建唯一的日志记录员再合适不过了,我们添加几行代码如下:

在这里插入图片描述
我们的receive也需要添加新参数isPush来获取用户是否点击了推送到云端,同时按照顺序,我们记录收到请求这条日志、再创建工程、再推送

在这里插入图片描述

调试

现在让我们启动服务,通过POSTMAN发送HTTP测试一下流程是否正常、日志是否打印

在这里插入图片描述

看看我们的控制台日志,各个角色已经正确记录了相应的日志,再也不怕日后扯皮了。

在这里插入图片描述

总结一下目前我们的4个角色之间的类图关系,大家注意菱形的方向,这是实战版的UML图,比其他文章那种阿猫、阿狗的举例更真实。

在这里插入图片描述

所以楼主鼓励大家多画图,用图去给别人说你的思路,比写连续剧更有画面感。图形翻译的是你的思想,与具体的框架都没有关系。

目前我们完成的代码,大概是20年前的代码风格…对,没错,代码暴露了以下问题:

new的问题

控制器交给了开发者,也就是我,我自己定义Class,我自己New,问题是业务场景下,全局我们只需要一个实例化对象,也就是单例模式,如果开发者自行完成new的过程,假如,我是说假如,有多名开发者共同维护代码,你说,有没有可能其他人也会New。。。这样全局就出现多个实例,多个实例不可怕,可怕的是维护和追踪问题。。。
在这里插入图片描述

由于目前我们的需求还很少,像ProjectLogger日志记录员只在创建工程这个用例中有使用,等系统越来越大,后面我们开发其他功能模块,比如用户管理的某个用例也想记录日志时,我们能做的无非是:

  1. 用户管理模块重新 new ProjectLogger()? 这样系统就出现两个一模一样的ProjectLogger日志记录员,大家觉得有必要吗?new就代表了内存的占用。其实我们更喜欢log方法而已,至于是谁提供的,真的不重要
  2. 再者,为了维持整个系统只有一个ProjectLogger日志记录员,我们可以通过把工程管理模块创建的日志记录员通过方法传递给用户管理模块,比如下面这样:
    在这里插入图片描述

但是,工程模块与用户模块完全就八竿子打不着,把代码写到这里简直是疯了…因此,类的创建控制权由开发者自己来维护是比较麻烦的事情。

依赖太具体,指名道姓了

ProjectLogger日志记录员为例,我们的其他几个类都依赖了它,而这个ProjectLogger日志记录员提供的log方法非常具体,就是打印日志到控制台。现在我们需求升级了,当本地调试环境时,打印控制台,当生产环境时,写入日志文件。哈哈哈哈,你说烦不烦,我相信大家平常开发肯定遇到过非常多类似问题。

肯定有很多同学会像下面这么实现,直接在我们之前定义的ProjectLogger日志记录员内部判断

在这里插入图片描述

好吧,知道为啥很多人写的代码经不起修改吗?因为太大杂烩了,假如之后我们的日志需求再次升级,那么你这个ProjectLogger日志记录员会经常添加这个环境判断的代码,你累不累。

从面向对象的思想分析,写文件的活应该由另外一个ProjectFileLogger文件日志记录员来完成,而之前的ProjectLogger日志记录员也应该改名为ProjectConsoleLogger控制台日志记录员更合适。请问,下面两个歌手,能一样吗?

在这里插入图片描述

面向对象思想还有个升级版:面向接口编程。以上面两个歌手为例,他们的共同点是会唱搁浅。以我们的日志记录员为例,他们的共同点是会写日志,我们开发的软件90%的场景都是由各种方法调用来堆砌出来的,不信你看看你们的代码是不是这样。

在这里插入图片描述

不知道你们有没有体会,反正我干了这么多年,需要的变更大多数情况是在上图的基础上完成变更:

  1. 添加几个方法(流程中增加新的用例,比如方法D调用方法F,之后再调用方法G,新增一个G)
  2. 或者把方法D换成方法D1(需求变了,但是需求前后方法的行为一样,只是方式改变)也就是可替代。

很多人搞不懂Typescript的接口interface是干啥的,接口就是抽象定义某种行为的,打游戏、吃饭、开车都是行为,写日志又何尝不是呢,我们定义一个日志员interface,注意interface定义方法时,返回类型如果是Promise,那么方法前面可以不写async关键字

在这里插入图片描述

大家能看懂吧,这个interface接口只定义写日志方法,方法要求的输入类型,以及方法的输出类型。而我们的ProjectFileLogger文件日志记录员ProjectConsoleLogger控制台日志记录员 这两个具体的类型分别实现写日志方法
在这里插入图片描述

对于调用方来说,看看之前的代码,依赖的日志记录员类型都太具体了(指名道姓),这就是所谓的紧耦合,不具备可替换性,专业术语叫:未遵守依赖倒置原则,注意不是里氏替换哦 这两个开发原则还是不同的。

依赖倒置:可替换,依赖抽象。
里氏替换:父类可以被子类替换

在这里插入图片描述

我们略微修改一下,只依赖抽象的能写日志的行为即可,无论它是写文件或者写控制台,只要能提供写日志的能力,我们的代码就万年不用改。修改一下我们的消息推送员

在这里插入图片描述

申请接收者同样需要修改,添加判断,不同的环境创建不同的具体的日志记录员,但是大家都是日志记录员。

在这里插入图片描述
在这里插入图片描述

至此,我们实现了第一个接口interface编程的案例。后期只要流程不变,我们的主干代码、调用关系是根本不需要变动的,比如当某某环境下,日志需要写入数据库,没关系,我们重新定义一个具体写入数据库的日志记录员,并且实现log方法即可。所以,从本章开始,大家就要建立面向接口编程的思想,其实很简单,你不管三七二十一,把所有方法都先定义到某个interface,然后再定义一个默认的实现类就行。后期哪怕不改动,你也不亏,知道吗?大家就按下图来开发就错不了

在这里插入图片描述

细心的同学肯定发现了,我们的ProjectCreator工程创建者提供了create方法,该方法把创建的实体临时存储在内存中,后期我们肯定要写入数据库,所以工程创建者也应该是抽象的。至于ProjectPusher消息推送员 目前是调用固定HTTP接口发送给其他系统,后期也有可能变成MQTT推送。按道理也应该是抽象的,但是为了一会讲依赖注入的对比,所以,我们暂且仅仅把工程创建者抽象成接口interface。

为了大家方便阅读,我把目前已经实现的核心代码按照调用链从底层至上贴出来

先是实体类,实体类不需要抽象,因为很具体

//实体类,实体类不需要抽象,因为很具体
export class Project {
  name: string;
  description: string;
  createdAt: string;
  updatedAt: string;
  createdBy: string;
  updatedBy: string;
  constructor(name: string, description: string, createdBy = 'admin') {
    this.name = name;
    this.description = description;
    this.createdBy = createdBy;
    this.createdAt = new Date().toISOString();
  }
}

定义2个抽象接口

//定义抽象接口
import { Project } from './project.entity';

//抽象写日志的行为
export interface LoggerProvider {
  log(who: string, message: string): Promise<boolean>;
}
//抽象工程管理的行为
export interface ProjectManagerProvider {
  create(name: string, description: string): Promise<Project>;
}

工程管理的具体实现类:在内存中管理

//工程管理的具体实现类:在内存里管理工程信息,没有写入数据库
import { Project } from './project.entity';
import { LoggerProvider, ProjectManagerProvider } from './project.interface';

//实现create方法
export class ProjectManagerInMemory implements ProjectManagerProvider {
  private projectLogger: LoggerProvider;//依赖抽象的写日志,而非具体控制台写日志
  constructor(projectLogger: LoggerProvider) {
    this.projectLogger = projectLogger;
  }

  async create(name: string, description: string): Promise<Project> {
    await this.projectLogger.log(`ProjectCreator`, `准备创建工程${name}`);
    //创建到内存
    const newP = new Project(name, description);
    return newP;
  }
}

写日志的具体实现类:控制台

//写日志的具体实现类:控制台
import { LoggerProvider } from './project.interface';
export class ProjectConsoleLogger implements LoggerProvider {
  async log(who: string, message: string): Promise<boolean> {
    console.log(`[${new Date().toISOString()}][${who}]:${message}`);
    return true;
  }
}

写日志具体实现类: 文件

//写日志具体实现类:文件
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import { LoggerProvider } from './project.interface';
export class ProjectFileLogger implements LoggerProvider {
  async log(who: string, message: string): Promise<boolean> {
    await fs.promises.appendFile(
      './log.txt',
      `[${new Date().toISOString()}][${who}]:${message}`,
    );
    return true;
  }
}

消息推送员就不抽象了,把ProjectPusher改名为ProjectHttpPusher

import { Project } from './project.entity';
import { LoggerProvider, ProjectPusherProvider } from './project.interface';
import * as http from 'http';
export class ProjectHttpPusher{
  private projectLogger: LoggerProvider;//依赖抽象的写日志,而非具体控制台写日志
  constructor(projectLogger: LoggerProvider) {
    this.projectLogger = projectLogger;
  }
  //具体实现pushToCloud
  async pushToCloud(newProject: Project): Promise<boolean> {
    //此处是示例代码
    const options = {
      hostname: 'cloud.com',
      port: 80,
      path: '/message',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    };
    //发送请求给cloud.com
    const req = http.request(options);
    //把项目信息发送过去
    req.write(JSON.stringify(newProject));
    req.end();
    //调用写日志方法,无需关注是写到控制台了还是写到文件了。
    await this.projectLogger.log('ProjectPusher', `新工程=${newProject.name}`);
    return true;
  }
}

最后是我们的申请接收者,申请接收者此时无需抽象,因为我们对外提供的就是HTTP访问,非常非常具体和肯定。

import { Controller, Post, Body } from '@nestjs/common';
import { ProjectManagerInMemory } from './project.provider';
import { Project } from './project.entity';
import { ProjectHttpPusher } from './projectPush.provider';
import {
  LoggerProvider,
  ProjectManagerProvider
} from './project.interface';
import { ProjectConsoleLogger } from './projectConsoleLogger.provider';
import { ProjectFileLogger } from './projectFileLogger.provider';

@Controller('/projects')
export class ProjectApplyReceiver {
 private projectHttpPusher: ProjectHttpPusher; //具体的HTTP消息推送接口
  private projectManager: ProjectManagerProvider; //抽象的工程管理接口
  private projectLogger: LoggerProvider; //抽象的日志写入接口
  constructor() {
    //不同环境变量,创建不同的日志记录员,但是所有的日志记录员都是实现了log方法
    if (process.env.NODE_ENV == 'deveplopment') {
      this.projectLogger = new ProjectConsoleLogger();
    } else {
      this.projectLogger = new ProjectFileLogger();
    }
    this.projectPusher = new ProjectHttpPusher(this.projectLogger);
    this.projectManager = new ProjectManagerInMemory(this.projectLogger);
  }
  @Post()
  async receive(
    @Body('name') name: string,
    @Body('description') description: string,
    @Body('isPush') isPush: boolean,
  ): Promise<string> {
    //无需关心是什么日志管理员,反正有log方法
    await this.projectLogger.log(
      `ProjectApplyReceiver`,
      `收到客户端请求name:${name}`,
    );
    //无需关心是什么工程创建者,反正有create方法
    const newProject: Project = await this.projectManager.create(
      name,
      description,
    );
    if (isPush) {
      //无需关心是什么消息推送,反正有pushToColud方法
      await this.projectPusher.pushToCloud(newProject);
    }
    return 'ok';
  }
}

讲到这里,同学们,我们这一路来的代码大致经历了以下几个阶段:

在这里插入图片描述

而编程领域的依赖注入就是为了解决我们这一路碰到的问题。看看官方的说法:

依赖注入(Dependency Injection,DI)的核心概念是将对象所需的依赖关系从对象本身外部注入进来(这个我们已经实现差不多了,通过构造函数注入了)而不是对象自己创建或管理这些依赖关系(我们目前还是自己手动创建

    if (process.env.NODE_ENV == 'deveplopment') {
      this.projectLogger = new ProjectConsoleLogger();
    } else {
      this.projectLogger = new ProjectFileLogger();
    }
    //下面两行代码就是手动版的依赖注入,手动创建然后注入
    this.projectHttpPusher = new ProjectHttpPusher(this.projectLogger);
    this.projectManager = new ProjectManagerInMemory(this.projectLogger);

Java的Spring框架是依赖注入的典型代表,注意看上面的代码,如果有框架能识别到我们代码之间的依赖关系,然后在合适的时候自动完成new操作,将十分利于开发人员来维护代码。想象一下系统越来越大,如果我们手动去new出来一个个的对象,到后面估计自己都搞晕了。

楼主之所以没有一开始就讲Nestjs是如何依赖注入,而是通过一步步的需求迭代来引导我们的代码进行变更优化,即出现问题、如何解决问题、框架如何帮我们更好的解决问题。

使用依赖注入

依赖注入可以自动识别到我们代码(类或者接口)的依赖关系,然后在特定条件下自动new。Nestjs实现了依赖注入这个概念,就跟Springboot实现了依赖注入一样。

依赖注入分3步:

第1步:记录依赖关系

有好几种途径来告诉Nestjs我们定义的类或者接口依赖了谁,一种是通过构造函数,另一种是通过成员方法。我们一个个来改造,先从申请接收者开始,它依赖了1个具体的消息推送员,1个抽象工程管理者,1个抽象的日志记录员。
目前的代码只是我们面向对象的原始写法,代码一点问题没有,现在我们改造成Nestjs的要求

面向对象原始写法:

@Controller('/projects')
export class ProjectApplyReceiver {
 private projectHttpPusher: ProjectHttpPusher; //具体的HTTP消息推送接口
  private projectManager: ProjectManagerProvider; //抽象的工程管理接口
  private projectLogger: LoggerProvider; //抽象的日志写入接口
  constructor() {
    //不同环境变量,创建不同的日志记录员,但是所有的日志记录员都是实现了log方法
    if (process.env.NODE_ENV == 'deveplopment') {
      this.projectLogger = new ProjectConsoleLogger();
    } else {
      this.projectLogger = new ProjectFileLogger();
    }
    this.projectPusher = new ProjectHttpPusher(this.projectLogger);
    this.projectManager = new ProjectManagerInMemory(this.projectLogger);
  }

Nestjs标准写法

@Controller('/projects')
export class ProjectApplyReceiver {
  constructor(
    @Inject('pp') private projectPusher: ProjectHttpPusher,
    @Inject('dd') private projectLogger: LoggerProvider,
    @Inject('ff')  private projectManager: ProjectManagerProvider,
  ) {}

这样一对比,首先我们主动new的代码不需要了,即类的控制创建权交给框架来处理(专业术语:控制反转)。而构造函数中出现了 @Inject 注解,拆分开进行解释如下:

   //1.请把叫pp的这个对象赋值给我的成员变量projectPusher
   //2.我的成员变量projectPusher是具体的ProjectHttpPusher类型
   @Inject('pp') private projectPusher: ProjectHttpPusher
   
   //1.请把叫dd的这个对象赋值给我的成员变量projectLogger
   //2.我的成员变量projectLogger是抽象的的LoggerProvider类型
   @Inject('dd') private projectLogger: LoggerProvider,

   

ok,我们启动下代码,看看效果,直接报错了,大致意思是无法解析依赖,dd,ff是什么?

在这里插入图片描述

第2步:声明式NEW

抱歉各位,虽然我们new的这些代码被省略了,但是,其实,并不是。只是要换一种方式去声明。
打开我们的模块类,project.module.ts 文件,目前仅仅声明该模块有一个controller控制器


@Module({
  controllers: [ProjectApplyReceiver]
})
export class ProjectModule {}

按照Nestjs的要求,实例化的过程变成了声明式.看到了没有,pp,dd,ff是什么?

完整版声明
@Module({
  controllers: [ProjectApplyReceiver],
  providers: [
    {
      provide: 'pp',  // 相当于 var pp;
      useClass: ProjectHttpPusher, //pp=new ProjectHttpPusher()
    },
    {
      provide: 'dd',// 相当于 var dd;
      useClass: ProjectConsoleLogger, // dd = new ProjectConsoleLogger()
    },
    {
      provide: 'ff',
      useClass: ProjectManagerInMemory,
    },
  ],
})
export class ProjectModule {}

调用接口看看,是否能控制台打印我们的日志

在这里插入图片描述

没毛病,现在默认声明的是创建控制台日志记录员,赋值给变量dd,现在我们改成文件日志记录员

   //修改前
   {
      provide: 'dd',// 相当于 var dd;
      useClass: ProjectConsoleLogger, // dd = new ProjectConsoleLogger()
    },
    
   //修改后
   {
      provide: 'dd',// 相当于 var dd;
      useClass: ProjectFileLogger, // dd = new ProjectFileLogger()
    }, 

再次调用接口,看看会不会写日志到log.txt文件中

在这里插入图片描述

完美,大家有没有发现声明式的实例化非常优雅,不污染主体代码,而是从侧面去指定具体new哪一个类。

判断式声明

但是我们日志记录员是要动态的哦,当我们的服务启动后,需要根据当前的环境,启用不同的日志记录员。没关系,加个判断就行,当开发环境时用ProjectConsoleLogger,生产环境用ProjectFileLogger

 {
      provide: 'dd',
      useClass:
        process.env.NODE_ENV === 'development'
          ? ProjectConsoleLogger
          : ProjectFileLogger,
    },
简写式声明

再来看消息推送员,由于目前只有一种实现,Nestjs提供了一种简写的方式,简写方式是大多数情况常用的,前提是只有一种实现,不存在像日志记录员、工程管理员这种有多种实现的情况。

 //修改前
 @Module({
  controllers: [ProjectApplyReceiver],
  providers: [
   {
      provide: 'pp',  //修改前完整版式声明
      useClass: ProjectHttpPusher,
    },
    {
      provide: 'dd',
      useClass:
        process.env.NODE_ENV === 'development'
          ? ProjectConsoleLogger
          : ProjectFileLogger,
    },
    {
      provide: 'ff',
      useClass: ProjectManagerInMemory,
    },
  ],
  //修改后
 @Module({
  controllers: [ProjectApplyReceiver],
  providers: [
   ProjectHttpPusher, //修改后简写版声明
   {
      provide: 'dd',
      useClass:
        process.env.NODE_ENV === 'development'
          ? ProjectConsoleLogger
          : ProjectFileLogger,
    },
    {
      provide: 'ff',
      useClass: ProjectManagerInMemory,
    },
  ],
})

 //上面的简写相当于
 {
 provide:'ProjectHttpPusher', // var ProjectHttpPusher;
 useClass:ProjectHttpPusher  // ProjectHttpPusher = new ProjectHttpPusher()
 }

变成简写后,我们一定要去依赖注入的地方修改 @Inject注解,之前注入的是叫pp的对象,现在要修改

//修改前
@Controller('/projects')
export class ProjectApplyReceiver {
  constructor(
    @Inject('pp') private projectPusher: ProjectHttpPusher,//以前注入叫pp的对象
    @Inject('dd') private projectLogger: LoggerProvider,
    @Inject('ff') private projectManager: ProjectManagerProvider,
  ) {}

//修改后
@Controller('/projects')
export class ProjectApplyReceiver {
  constructor(
    @Inject('ProjectHttpPusher') private projectPusher: ProjectHttpPusher,//现在换成叫ProjectHttpPusher的对象
    @Inject('dd') private projectLogger: LoggerProvider,
    @Inject('ff') private projectManager: ProjectManagerProvider,
  ) {}
 

进一步修改,Nestjs支持当注入的对象名与依赖的类型同名时,可以省略 @Inject 注解

//简写后
@Controller('/projects')
export class ProjectApplyReceiver {
  constructor(
    private projectPusher: ProjectHttpPusher,
    @Inject('dd') private projectLogger: LoggerProvider,
    @Inject('ff') private projectManager: ProjectManagerProvider,
  ) {}
 

朋友们,学到了吗,楼主的讲解方式与Nestjs官方的刚好相反。因为我认为只有理解了完整版的方式,才能更好的理解简写版,反之不成立。

在这里插入图片描述

工厂函数声明

继续看,当我们的服务启动的时候,我们默认用的是存储在内存中的工程创建者,并且用ff这个名字作为实例化后的名称

{
      provide: 'ff',
      useClass: ProjectManagerInMemory,
    },

然后注入到了申请接收者,注意看 ProjectManagerProvider 这是一个抽象的interface

@Controller('/projects')
export class ProjectApplyReceiver {
  constructor(
    private projectPusher: ProjectHttpPusher,
    @Inject('dd') private projectLogger: LoggerProvider,
    @Inject('ff')
    private projectManager: ProjectManagerProvider,
  ) {}
  @Post()

接下来我们要做一件事情,我们的需求升级了,创建工程时,用户可以指定工程信息存储到内存或者JSON文件。原型图如下:

在这里插入图片描述
也就是我们不能一直使用内存工程创建者,而是需要根据用户的选择(也就是HTTP请求不同)动态的在 内存工程创建者JSON工程创建者 中2选1。 工厂模式 你们都听过吧,我们定义个工厂类,这个类有3个依赖,一个是Nestjs框架提供的名叫REQUEST的对象,另外两个是我们自己定义的工程创建者。

import { Inject, Injectable, Scope } from '@nestjs/common';
import { ProjectManagerInJson } from './projectManagerInJson.provider';
import { ProjectManagerInMemory } from './projectManagerInMemory.provider';
import { Request } from 'express';
import { REQUEST } from '@nestjs/core';

export class ProjectManagerFactory {
  private id: number;
  constructor(
    @Inject(REQUEST) private request: Request,
    private inMemory: ProjectManagerInMemory,
    private inJson: ProjectManagerInJson,
  ) {
    this.id = new Date().getTime();
  }
  public getManager() {
    const type = this.request.body?.type;
    switch (type) {
      case 'MEMORY':
        return this.inMemory;
      case 'JSON':
        return this.inJson;
      default:
        return this.inMemory;
    }
  }
}

注入的REQUEST这个对象,在Nestjs内部核心模块Core声明好的,类似下面:


@Module({
 providers:[
    {
       provide:'REQUEST',//声明名为REQUEST的对象
       .....
     }
  ]
})
export class CoreModule{}

我们开发者可以去注入这个对象,这个对象的类型是啥?就是express 包装的HTTP 请求对象,我们从这个对象上面可以获取请求的一切信息。并且,非常关键,Nestjs说了,哪个Class注入这个对象,那么每次来请求的时候,我都把这个Class重新new一次。

看到没,我们之前定义的类都只会在服务启动后被new一次(全局只有一个实例),而一旦注入了REQUEST,就变成了一次请求,new一次。由单例变成多例。

另外,我们还依赖了两个简写的工程创建者,这两个可不是Nesjt内置的哦,需要我们自己去声明。

private inMemory: ProjectManagerInMemory,
private inJson: ProjectManagerInJson,

来到我们的模块类,新添加ProjectManagerFactory,ProjectManagerInJson,ProjectManagerInMemory这三个简写版声明,为啥可以简写,翻翻上面的内容。。。

@Module({
  controllers: [ProjectApplyReceiver],
  providers: [
    ProjectHttpPusher,
    {
      provide: 'dd',
      useClass:
        process.env.NODE_ENV === 'development'
          ? ProjectConsoleLogger
          : ProjectFileLogger,
    },
    {
      provide: 'ff',
      useFactory: (pmf: ProjectManagerFactory) => { //声明为动态工厂创建
        console.log(`call ff useFactory`);
        return pmf.getManager();
      },
      inject: [ProjectManagerFactory],
    },
    ProjectManagerFactory, //简写
    ProjectManagerInJson, //简写
    ProjectManagerInMemory,//简写
  ],
})
export class ProjectModule {}

直接测试一下

在这里插入图片描述

在这里插入图片描述
看到没,写入json文件了。我们全程没有修改申请接收者controller,这就是面向抽象和切面的魅力。

注入ff对象给到ProjectManagerProvider类型,ProjectManagerProvider是个抽象的interface,而ff对象根据请求,动态选择其中某个创建者。

@Controller('/projects')
export class ProjectApplyReceiver {
  constructor(
    private projectPusher: ProjectHttpPusher,
    @Inject('dd') private projectLogger: LoggerProvider,
    @Inject('ff')
    private projectManager: ProjectManagerProvider,
  ) {}
  @Post()
  async receive(
    @Body('name') name: string,
    @Body('description') description: string,
    @Body('isPush') isPush: boolean,
  ): Promise<string> {
    //无需关心是什么日志管理员,反正有log方法
    await this.projectLogger.log(
      `ProjectApplyReceiver`,
      `收到客户端请求name:${name}`,
    );
    //无需关心是什么工程创建者,反正有create方法
    const newProject: Project = await this.projectManager.create(
      name,
      description,
    );
    if (isPush) {
      //无需关心是什么消息推送,反正有pushToColud方法
      await this.projectPusher.pushToCloud(newProject);
    }
    return 'ok';
  }
}
值声明
{
      provide: 'util',
      useValue: {
        getRandomFloat(min, max) {
          return Math.random() * (max - min) + min;
        },
      },
    },

这样我们就可以注入给别人使用,一般注入到时候,由于uitl并不是具体的某个类型,所以可以设置为any。当然如果你要把ts用的很扎实,也可以描述这个类型

@Inject('util') utilTool:any

添加utilTool描述,单纯描述类型用type关键字就可以,不需要用Class或者interface。type描述看起来和interface很像,都是只定义,不实现。区别是意义不同,这个需要大家自己用心感悟。

type UtilTool = {
  getRandomFloat(min,max):number
}

重新注入,不是any了

@Inject('util') utilTool:UtilTool

好了, 各位同学,这一章节我写的头都麻了,因为我一直在考虑如果用真实的需求来推动代码的优化,进而引出依赖注入,累了,累了,休息几天,继续。觉得有学到的,点赞关注,donate打赏一下也可以哦,爱你们。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秦人阿超

创作不易,如果帮到你了,感谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值