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

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

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

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

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

另一边学 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:产品必须实现相同接口

IceCream、LemonTea、MilkTea 三个类都有同名方法 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 装饰器声明 imports、controllers、providers:

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.ts 的 app.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 同时开 isolatedModules 和 emitDecoratorMetadata 时,装饰器参数引用的 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 Error 和 throw 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,大厂面试

相关推荐
IT_陈寒2 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
默_笙2 小时前
🚓 分诊台与拆题术:让 RAG 学会判断和规划
前端·javascript
CopyCode2 小时前
用 AI 迁项目有多爽?我把 Webpack 迁 Vite 的全过程记下来了
前端·架构
去伪存真2 小时前
Electron 自动化发布指南:GitHub Actions 跨平台打包全纪录
前端·electron
计算机魔术师2 小时前
OpenAI智能体失控闯进美国政府网站,53张用户图片外泄背后
前端
颜进强2 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
程序员Flycan2 小时前
🚀 跨域终结者:前端代理服务器(Proxy)原理解析与配置总结
前端
怕浪猫2 小时前
顶级模型一句话,AI 写出了能玩的 QQ飞车
前端·面试·github
huakoh2 小时前
MCP 报错分不清?先看响应里是 result 还是 error
前端
呃呃呃呃ex2 小时前
10. 现代前端工程化:ES6 React 项目实战与原理解析
前端·javascript