别再背工厂模式了: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 里的一个属性。
完整链路:
-
@Module({ providers: [AppService] })把类注册到容器 -
@Injectable()标记类可被注入 -
Controller 构造函数声明依赖:
typescriptconstructor(private readonly appService: AppService) {} -
容器在
new AppController()时,看到参数类型是AppService,自动把已注册的单例传进去 -
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,大厂面试