别再背工厂模式了:NestJS 第一行代码就是它的工业级落地

别再背工厂模式了:NestJS 第一行代码就是它的工业级落地

本文写给一类人:学完设计模式背完面试题,一写 NestJS 第一行 NestFactory.create(AppModule) 却没意识到这就是工厂模式。文章只有一个核心判断------工厂模式不是抽象理论,而是「调用方不直接 new、通过工厂统一入口拿对象」的工程实践,NestJS 启动第一行就是它的工业级落地 。文中代码来自本地学习项目静态阅读,运行未验证,结论以 NestJS 官方文档为准。

一、问题:设计模式学完用不上

很多人学设计模式的体验是这样:背完「创建型/结构型/行为型」三大类,记下「工厂模式是 23 种之一,面向接口编程」,面试能答「工厂模式解决对象创建问题」------然后呢?写代码时一个工厂都没用过,照样 newnew 去。

另一边学 NestJS,新建项目 main.ts 第一行就是:

typescript 复制代码
const app = await NestFactory.create(AppModule);
await app.listen(3000);

教程告诉你「这是创建应用实例」,但没人告诉你这其实就是工厂模式。结果设计模式和工程实践成了两张皮。

本文要做的事很简单:用一段 50 行 JS 代码(蜜雪冰城示例)讲清工厂模式的最小实现,再把它和 NestFactory.create(AppModule) 一一对应。读完之后,你能用一个具体的工业级落地点回答面试官「你在哪里用过工厂模式」。

二、50 行 JS 看懂工厂模式

不要先把「面向接口编程」「创建型模式」这些词搬出来。直接看代码(来自本地学习项目 factory_demo/1.js运行未验证):

javascript 复制代码
// 产品类:3 种蜜雪冰城产品,都实现 show() 方法
class IceCream {
  constructor() { this.name = '冰激凌'; this.price = 3; }
  show() { console.log(`${this.name} ${this.price}元`); }
}
class LemonTea {
  constructor() { this.name = '柠檬水'; this.price = 4; }
  show() { console.log(`${this.name}, ${this.price}元`); }
}
class MilkTea {
  constructor() { this.name = '珍珠奶茶'; this.price = 8; }
  show() { console.log(`${this.name}, ${this.price}元`); }
}

// 工厂类
class MixueFactory {
  static create(type) {
    switch(type) {
      case 'ice':  return new IceCream();
      case 'lemon': return new LemonTea();
      case 'milk':  return new MilkTea();
    }
  }
}

// 调用方只跟工厂打交道
const drink1 = MixueFactory.create('ice');
drink1.show();   // 冰激凌 3元

工厂模式的全部「奥秘」就在这 50 行里。

关键判断 1:产品必须实现相同接口

IceCreamLemonTeaMilkTea 三个类都有同名方法 show()。这不是巧合,是工厂模式成立的前提 :所有产品必须能用统一接口调用,否则工厂返回后调用方还得写 if (drink instanceof IceCream) ...,那要工厂干什么。

源码注释直接点出(factory_demo/1.js#L2-L5):

text 复制代码
// 企业, 很多的产品, 每一种产品都实现了想同的接口(方法),
// 一个企业这么多产品, 开发这怎么记得住? 还有那么多工厂呢?
// 工厂模式来搞, 你不需要了解工厂里面那么多类的实现细节,
// 只要直接和工厂类打交道就好了

关键判断 2:调用方不直接 new

MixueFactory.create(type) 是工厂的统一入口,内部 switch 决定 new 哪个具体产品类。new 的动作只发生在工厂内部,调用方永远看不到 new IceCream() 这种代码

这就是工厂模式的本质------不是「工厂内部怎么写」,而是「调用方不再直接 new」。

关键判断 3:解耦的代价由工厂承担

不用工厂:调用方写 new IceCream() new LemonTea() new MilkTea(),每加一个产品调用方就要改代码。 用工厂:调用方只写 MixueFactory.create(type),新增产品时只改工厂内部 switch 一个分支。

代价是工厂内部要维护一份产品清单。但这份清单集中在一处,比分散在所有调用方好维护得多。这就是 factory_demo/readme.md#L9-L12 说的:

以实现 MixueFactory 帮我们提供工厂里的各种类,和工厂里五花八门的类解耦。

三、NestFactory.create 是同一个模式

学习项目 factory_demo/readme.md#L8 直接点名:

NestFactory 蜜雪冰城 满足做App的需要

把 NestJS 的 main.ts 翻译出来(基于 NestJS 官方约定推导,运行未验证):

typescript 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

把这段和上面的蜜雪冰城代码对照:

蜜雪冰城 NestJS 同构关系
MixueFactory NestFactory 工厂类
create('ice') create(AppModule) 工厂方法 + 类型标识
IceCream/LemonTea/MilkTea AppModule 实现了约定接口的产品类
show() 同名方法 @Module({imports, controllers, providers}) 产品遵循的接口约定
drink.show() app.listen(3000) 拿到实例后调用统一方法

AppModule 为什么算产品类

AppModule 实现了 NestJS 约定的接口------通过 @Module 装饰器声明 importscontrollersproviders

typescript 复制代码
@Module({
  imports: [TodosModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

NestFactory.create(AppModule) 拿到这个产品类后,按约定读取元数据,自动装配依赖注入容器、注册路由、启动 HTTP 监听。开发者只管 app.listen(3000),不关心内部细节------和「拿到 drink 后只管 drink.show()」完全同构。

这才是工厂模式的工业级落地

教科书讲工厂模式,例子往往是 AnimalFactory.create('dog'),看起来很玩具。NestJS 的 NestFactory.create(AppModule) 是工业级框架的启动入口------它要处理依赖注入容器装配、HTTP server 启动、生命周期钩子、全局模块注册等一大堆内部细节,但调用方(开发者)一行 await NestFactory.create(AppModule) 就拿到能用的 app 实例。

复杂度藏在工厂内部,简单接口暴露给调用方------这才是工厂模式在工业代码里的真正价值。

四、NestJS 启动之后的请求链路

工厂把 app 实例给你之后,一次请求怎么流转?readme.md#L23-L44 给了完整链路图:

text 复制代码
浏览器访问 http://localhost:3000
        ↓
main.ts 的 app.listen(3000) 接收请求
        ↓
NestJS 路由匹配:GET /
        ↓
找到 AppController(由 AppModule 注册)
        ↓
AppController 的 @Get() 装饰器匹配
        ↓
调用 getHello() 方法
        ↓
console.log('/ 的控制器')
        ↓
调用 this.appService.getHello()
        ↓
AppService.getHello() 返回 'Hello World!'
        ↓
Controller 返回给浏览器
        ↓
浏览器显示 Hello World!

这条链路里藏着 NestJS 的三大支柱------模块化装饰器依赖注入。下面分别拆。

五、NestJS 三大支柱的分工

模块化:用 @Module 划分业务领域

readme.md#L56-L57

Module 是 NestJS 的独立业务模块。 xx.module.ts 定义和组装。

每个业务领域一个模块,根模块通过 imports 把其他模块组合起来:

typescript 复制代码
@Module({
  imports: [TodosModule],          // 组合其他模块
  controllers: [AppController],    // 注册控制器
  providers: [AppService],         // 注册服务
})
export class AppModule {}

大型项目的代码组织靠这个机制保持清晰------每个模块自包含自己的控制器和服务,模块之间通过 imports 显式声明依赖。

装饰器:动态添加功能不改业务代码

readme.md#L50-L52

装饰器模式在不修改原有对象的前提下,动态添加额外功能。

readme.md#L62 直接评价:

装饰器模式用到极致。

NestJS 三类装饰器对照表:

类型 示例 作用
类装饰器 @Controller('todos') @Module({...}) @Injectable() 路由前缀、模块元数据、可注入标记
方法装饰器 @Get() @Post() @Put() @Delete() 绑定 HTTP 方法到路由
参数装饰器 @Param('id') @Body('title') @Body() @Query('page') 从请求提取数据注入参数

好处是业务类保持纯净------getHello() 方法本身只有业务逻辑,框架关心的元数据(路由、HTTP 方法、参数提取)通过装饰器叠加。业务代码和框架代码物理上同一文件,逻辑上完全分离

依赖注入:构造函数声明依赖,容器自动注入

readme.md#L58-L60

@Injectable() 自动依赖注入。 自动注入 controller 或任何用它的地方。 controller 里的一个属性。

完整链路:

  1. @Module({ providers: [AppService] }) 把类注册到容器

  2. @Injectable() 标记类可被注入

  3. Controller 构造函数声明依赖:

    typescript 复制代码
    constructor(private readonly appService: AppService) {}
  4. 容器在 new AppController() 时,看到参数类型是 AppService,自动把已注册的单例传进去

  5. Controller 内部 this.appService.getHello() 即可调用

价值不只是少写一行 new------解耦 (Controller 不依赖 Service 的具体实现)、可测试 (单测传 Mock)、单例复用(整个应用共享一个实例)。

六、内置错误类:单线程崩溃的最后防线

readme.md#L65-L71 抛了一个面试题:

请说下你是如何处理后端报错的? try catch finally ts 独苗 线程挂了。 nest.js 提供了各种错误类,标准化错误输出。 status code 状态码。message 消息。

为什么必须统一错误处理

readme.md#L67 那句「ts 独苗 线程挂了」指的是 Node 后端的根本痛点------单线程一旦未捕获异常冒泡到事件循环,整个进程崩溃退出 。不像 Java 多线程,挂一个线程不影响其他线程。Node 必须有一套统一错误兜底机制,否则一个 throw new Error 就能让线上服务全挂。

NestJS 内置 HTTP 异常类

HTTP 状态码 用途
BadRequestException 400 参数错误
UnauthorizedException 401 未登录
ForbiddenException 403 已登录但无权限
NotFoundException 404 资源不存在
ConflictException 409 资源冲突
InternalServerErrorException 500 服务器内部错误

这些类继承自 HttpException,抛出后框架自动转成对应 HTTP 状态码 + JSON 响应,不需要手写 res.status(404).send(...)

实战对比:

typescript 复制代码
// 错误写法:throw new Error 会被 NestJS 默认转成 HTTP 500
if (!item) throw new Error("Item " + id + " not found");

// 正确写法:内置异常类自动返回 404
if (!item) throw new NotFoundException("Item " + id + " 不存在");

后者语义正确------「查不到数据」该返回 404 而不是 500。前端拿到 500 会以为「服务器坏了」,拿到 404 才知道「没这条数据」。

七、5 个面试问答

Q1:工厂模式是什么?为什么用?

工厂模式是 23 种设计模式中最重要的创建型模式。核心是「面向接口编程」------调用方不直接 new 具体类,而是通过工厂的统一入口拿对象。价值是调用方与具体产品类解耦,新增产品时调用方无需改代码。

Q2:NestJS 的请求处理流程?

浏览器 → main.tsapp.listen 接收 → NestJS 路由匹配 → 找到 @Controller → 匹配 @Get@Post 装饰器 → 调用对应方法 → 方法内调 this.appService.getHello() → Service 返回数据 → Controller 返回浏览器 → 浏览器渲染。

Q3:NestJS 为什么是「装饰器模式用到极致」?

几乎所有元数据都通过装饰器附加------@Module 注册模块、@Controller 声明路由前缀、@Get@Post 绑定 HTTP 方法、@Injectable 标记可注入、@Param@Body@Query 提取请求参数。装饰器的好处是「在不修改原有对象的前提下,动态添加额外功能」,让业务代码保持纯净。

Q4:NestJS 内置错误类解决什么问题?

解决「TS 单线程未捕获异常会让进程崩溃」的痛点。提供一套继承自 HttpException 的标准错误类,抛出后框架自动转成对应 HTTP 状态码 + JSON 错误响应,避免手写 res.status().send(),配合全局 Exception Filter 可统一兜底。

Q5:NestFactory.create(AppModule) 怎么算工厂模式?

NestFactory 是工厂类,create 是静态工厂方法,AppModule 是实现了 NestJS 约定接口(@Module + controllers + providers)的「产品类」。开发者只管 const app = await NestFactory.create(AppModule); await app.listen(3000);,不需要关心内部如何装配依赖注入容器、如何启动 HTTP server------这正是工厂模式的解耦价值。

八、面试加分项:踩过的坑

坑 1:TS1272 报错

tsconfig 同时开 isolatedModulesemitDecoratorMetadata 时,装饰器参数引用的 interface 必须用 import type

typescript 复制代码
// 错误
import { TodosService, TodoItem } from "./Todos.service";

// 正确
import { TodosService, type TodoItem } from "./Todos.service";

报错码 TS1272。原因:interface 编译后会被擦除,但装饰器元数据又需要它在运行时存在,TS 强制用 import type 显式声明「仅类型」。

坑 2:Partial 的 Mass Assignment 漏洞

Partial<T> 把所有属性变可选,包括 id 这种不应被外部修改的字段。前端发 { "id": 999 } 配合 Object.assign(target, patch) 会把目标对象的 id 改掉。这是 OWASP Mass Assignment 漏洞。

修复:

typescript 复制代码
type TodoUpdateDTO = {
  title?: string;
  complete?: boolean;
};
update(id: number, patch: TodoUpdateDTO): TodoItem {
  const item = this.findOne(id);
  Object.assign(item, patch);
  return item;
}

坑 3:pnpm build 在子目录报 exit code 1

src/ 下跑 pnpm build,pnpm 找不到 package.json 会触发 install 失败。修复:cd 到项目根目录再跑。

坑 4:[ERR_PNPM_IGNORED_BUILDS] 警告

pnpm v9+ 默认安全策略拦截 install scripts(如 unrs-resolver@1.12.2),非致命警告,不影响构建。要消掉跑 pnpm approve-builds 放行。

九、复习路线

  • 当天:默写 50 行蜜雪冰城代码;讲一遍 NestJS 请求链路(5 步以内)。
  • 1 天后 :实操 nest new 创建项目,跑通 npm run start:dev,访问 http://localhost:3000 看到 Hello World!
  • 3 天后 :给项目加一个新接口(如 GET /todos/:id),故意访问不存在的 id 触发 NotFoundException,对比 throw new Errorthrow new NotFoundException 的响应差异(500 vs 404)。
  • 7 天后:默写完整知识地图;准备 3 分钟口述「我是怎么处理后端报错的」。

十、未解决问题

  • NestFactory.create(AppModule) 内部具体做了什么?本文只给「工厂模式」视角的解读,未展开 NestJS 源码层面的装配细节。建议查 NestJS 官方文档「Application Context」「Lifecycle」。
  • 23 种设计模式中除工厂模式外的其他模式(特别是 NestJS 也用到的:装饰器、单例、依赖注入)未覆盖。

结语

工厂模式不是抽象的设计模式理论,而是「调用方不直接 new、通过工厂统一入口拿对象」的工程实践。MixueFactory.create('ice')NestFactory.create(AppModule) 是同一个模式在两种语境下的落地------前者是教学例子,后者是工业级框架的启动入口。

大厂面试爱问的不是「工厂模式是什么」(这种答案人人都会背),而是「你在哪里用过」「为什么这么设计」。NestFactory.create(AppModule) 就是一个真实可讲的落地点------它不是教科书例子,而是工业级框架启动的第一行代码。

下次再有人问「工厂模式用在哪」,别再讲 AnimalFactory.create('dog') 了,讲 NestFactory.create(AppModule)


标签:NestJS,设计模式,工厂模式,TypeScript,大厂面试

相关推荐
光影少年43 分钟前
RN 常见性能问题:JS卡顿、UI卡顿、桥接通信耗时
前端·react native·react.js
liuxiaocheng1 小时前
文本生成的进阶:generateText / streamText 里迟早会撞上的东西
前端·后端·ai编程
渣波1 小时前
深度解析工厂模式:从蜜雪冰城到 NestFactory,彻底搞懂“创建与使用分离”
前端·javascript
蔓越莓1 小时前
打包工具:编译器ESBuild
前端·面试
用户921080262861 小时前
Bubble 的 loading 和 typing:AI 回复生成中的交互处理
前端
SamChan901 小时前
用Playwright端到端测试PDF翻译功能:Web自动化测试实战
前端·python·ai·pdf·wpf
平生不晚1 小时前
在 SVG 体系里画一条任意的线
前端·算法
用户059540174461 小时前
把大模型记忆召回测试从 30 分钟手工核对压到 3 秒自动化,pytest + FAISS 这套组合救了我
前端·css
醉里博客1 小时前
醉里起始页 Snavigation2.0:纯前端导航页的二开改造与性能优化实践
前端