创建型模式(下)—— 建造者模式、原型模式
有些对象不是一步就能造好的,它像一道工序繁多的菜品,需要一步步组装;还有些对象的创建成本很高,直接”克隆”一份比从头构建要划算得多。建造者模式和原型模式分别应对这两类场景。
📋 开篇自测:你已经知道多少?
- TypeScript 中构造函数参数超过 5 个时,你通常如何处理?
- JavaScript 的
Object.assign和structuredClone分别实现的是什么层级的拷贝?- 你能说出”建造者模式”和”工厂模式”的核心区别吗?
一、建造者模式:分步构造复杂对象
1.1 从 HTTP 请求构造器说起
假设你正在封装一个 HTTP 客户端库。一个 HTTP 请求有大量可选配置:URL、方法、请求头、请求体、超时时间、重试策略、认证信息、缓存策略等等。如果把这些全放到构造函数里:
// 反面教材:参数爆炸的构造函数
const request = new HttpRequest(
'https://api.example.com/users',
'POST',
{ 'Content-Type': 'application/json' },
JSON.stringify({ name: 'Alice' }),
5000,
3,
'Bearer xxx',
'no-cache',
true, // 这个 true 是什么意思?followRedirect?
false // 这个 false 呢?
);
这段代码有几个严重的问题:调用者根本看不懂每个参数的含义;如果只想设置 URL 和超时时间,中间的参数全都要传默认值;参数顺序搞反了也不会报错。
你可能会说”用一个配置对象不就行了”——没错,但配置对象有时也不够。比如有些配置之间存在依赖关系(设置了认证方式才能设置 token),有些配置需要校验(超时时间必须大于 0),有些配置有复杂的默认值逻辑。这些都需要在构建过程中处理,而不是在一个扁平的配置对象里一股脑塞进去。
建造者模式的核心思想是:将一个复杂对象的构造过程与它的最终表示分离,使得同样的构造过程可以创建不同的表示。
1.2 TypeScript 中的建造者实现
// 最终产品
class HttpRequest {
readonly url: string;
readonly method: string;
readonly headers: Record<string, string>;
readonly body: string | null;
readonly timeout: number;
readonly retryCount: number;
readonly authToken: string | null;
readonly cachePolicy: string;
// 构造函数是私有的,外部只能通过 Builder 创建
private constructor(builder: HttpRequestBuilder) {
this.url = builder.getUrl();
this.method = builder.getMethod();
this.headers = builder.getHeaders();
this.body = builder.getBody();
this.timeout = builder.getTimeout();
this.retryCount = builder.getRetryCount();
this.authToken = builder.getAuthToken();
this.cachePolicy = builder.getCachePolicy();
}
// 静态工厂方法:在类内部调用私有构造函数,供 Builder 使用
static fromBuilder(builder: HttpRequestBuilder): HttpRequest {
return new HttpRequest(builder);
}
}
// 建造者
class HttpRequestBuilder {
private url = '';
private method = 'GET';
private headers: Record<string, string> = {};
private body: string | null = null;
private timeout = 10000;
private retryCount = 0;
private authToken: string | null = null;
private cachePolicy = 'default';
setUrl(url: string): this {
if (!url.startsWith('http')) {
throw new Error('URL 必须以 http 开头');
}
this.url = url;
return this; // 返回 this 支持链式调用
}
setMethod(method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'): this {
this.method = method;
return this;
}
addHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
setBody(body: object): this {
this.body = JSON.stringify(body);
// 自动设置 Content-Type,这就是"构建过程中的智能逻辑"
if (!this.headers['Content-Type']) {
this.headers['Content-Type'] = 'application/json';
}
return this;
}
setTimeout(ms: number): this {
if (ms <= 0) throw new Error('超时时间必须大于 0');
this.timeout = ms;
return this;
}
setRetry(count: number): this {
if (count < 0 || count > 10) throw new Error('重试次数必须在 0-10 之间');
this.retryCount = count;
return this;
}
setBearerAuth(token: string): this {
this.authToken = token;
this.headers['Authorization'] = `Bearer ${token}`;
return this;
}
setCachePolicy(policy: 'default' | 'no-cache' | 'force-cache'): this {
this.cachePolicy = policy;
return this;
}
// Getter 方法供 HttpRequest 构造函数使用
getUrl() { return this.url; }
getMethod() { return this.method; }
getHeaders() { return { ...this.headers }; }
getBody() { return this.body; }
getTimeout() { return this.timeout; }
getRetryCount() { return this.retryCount; }
getAuthToken() { return this.authToken; }
getCachePolicy() { return this.cachePolicy; }
build(): HttpRequest {
if (!this.url) throw new Error('URL 是必填项');
// 通过静态方法调用私有构造函数(外部类无法直接 new HttpRequest)
return HttpRequest.fromBuilder(this);
}
}
使用起来清晰明了:
const request = new HttpRequestBuilder()
.setUrl('https://api.example.com/users')
.setMethod('POST')
.setBody({ name: 'Alice', role: 'admin' })
.setBearerAuth('eyJhbGciOiJIUzI1NiJ9...')
.setTimeout(5000)
.setRetry(3)
.build();
// 每个配置的含义一目了然
// 参数之间的依赖关系由 Builder 内部处理
// 必填项校验在 build() 时统一执行
1.3 Director 角色:封装常用构建流程
当某些构建步骤组合经常被复用时,可以引入 Director(指挥者)角色来封装这些”预设”:
class HttpRequestDirector {
// 预设:标准的 JSON API 请求
static jsonApiRequest(url: string, token: string): HttpRequestBuilder {
return new HttpRequestBuilder()
.setUrl(url)
.addHeader('Accept', 'application/json')
.setBearerAuth(token)
.setTimeout(8000)
.setRetry(2);
}
// 预设:文件上传请求
static fileUploadRequest(url: string): HttpRequestBuilder {
return new HttpRequestBuilder()
.setUrl(url)
.setMethod('POST')
.addHeader('Content-Type', 'multipart/form-data')
.setTimeout(60000)
.setRetry(0);
}
}
// 使用预设,再自定义个别参数
const request = HttpRequestDirector
.jsonApiRequest('https://api.example.com/reports', myToken)
.setMethod('PUT')
.setBody({ status: 'published' })
.build();
1.4 建造者 vs 工厂:何时用谁
| 维度 | 建造者模式 | 工厂模式 |
|---|---|---|
| 关注点 | 分步构造过程 | 选择哪个具体类 |
| 产品复杂度 | 属性多、配置复杂 | 可能很简单 |
| 构造步骤 | 多步,可选可必填 | 通常一步到位 |
| 典型场景 | SQL查询构造器、HTTP请求、复杂配置对象 | 根据类型创建不同的通知/支付/日志对象 |
简单来说:工厂关心”造什么”,建造者关心”怎么造”。
🤔 想一想 TypeScript 社区中流行的 ORM 库(如 Prisma、TypeORM)的查询构造 API(如
queryBuilder.where(...).orderBy(...).limit(...))使用了建造者模式吗?它和经典的建造者模式有什么异同?
二、原型模式:通过克隆创建对象
2.1 从表单模板系统说起
假设你正在开发一个在线问卷系统。用户可以创建问卷模板,每个模板包含标题、描述、题目列表、样式配置等。当用户想基于某个模板创建新问卷时,需要复制一份模板的全部内容,然后在此基础上修改。
直接赋值?那是浅拷贝的灾难:
interface Question {
id: string;
type: 'text' | 'radio' | 'checkbox';
title: string;
options?: string[];
}
interface SurveyTemplate {
title: string;
description: string;
questions: Question[];
style: {
primaryColor: string;
fontSize: number;
showProgressBar: boolean;
};
}
// 灾难现场
const template: SurveyTemplate = {
title: '客户满意度调查',
description: '请花 2 分钟填写此问卷',
questions: [
{ id: 'q1', type: 'radio', title: '您的满意度', options: ['非常满意', '满意', '一般', '不满意'] },
{ id: 'q2', type: 'text', title: '您的建议' },
],
style: { primaryColor: '#1890ff', fontSize: 14, showProgressBar: true },
};
const newSurvey = { ...template }; // 浅拷贝!
newSurvey.title = '2026年度调查'; // 这行没问题
newSurvey.questions[0].title = '请评价本次服务'; // 灾难:模板也被改了!
原型模式的核心思想是:通过复制一个已有对象来创建新对象,而不是从头构造。 它要求被复制的对象(原型)提供一个 clone 方法,返回自身的深拷贝。
2.2 TypeScript 中的原型模式实现
// 可克隆接口
interface Cloneable<T> {
clone(): T;
}
class Question implements Cloneable<Question> {
constructor(
public id: string,
public type: 'text' | 'radio' | 'checkbox',
public title: string,
public options: string[] = []
) {}
clone(): Question {
return new Question(
this.id,
this.type,
this.title,
[...this.options] // 数组需要展开创建新副本
);
}
}
class SurveyStyle implements Cloneable<SurveyStyle> {
constructor(
public primaryColor: string,
public fontSize: number,
public showProgressBar: boolean
) {}
clone(): SurveyStyle {
return new SurveyStyle(
this.primaryColor,
this.fontSize,
this.showProgressBar
);
}
}
class Survey implements Cloneable<Survey> {
constructor(
public title: string,
public description: string,
public questions: Question[],
public style: SurveyStyle
) {}
clone(): Survey {
return new Survey(
this.title,
this.description,
this.questions.map(q => q.clone()), // 深拷贝每个题目
this.style.clone() // 深拷贝样式
);
}
addQuestion(question: Question): void {
this.questions.push(question);
}
}
使用:
// 创建模板
const template = new Survey(
'客户满意度调查',
'请花 2 分钟填写此问卷',
[
new Question('q1', 'radio', '您的满意度', ['非常满意', '满意', '一般', '不满意']),
new Question('q2', 'text', '您的建议'),
],
new SurveyStyle('#1890ff', 14, true)
);
// 基于模板克隆新问卷
const newSurvey = template.clone();
newSurvey.title = '2026年度客户调查';
newSurvey.questions[0].title = '请评价本次服务';
// 模板不受影响
console.log(template.questions[0].title); // '您的满意度' —— 安全!
console.log(newSurvey.questions[0].title); // '请评价本次服务'
2.3 深拷贝的三种常见方案
在 TypeScript 中,实现深拷贝有多种方式,各有优劣:
方案一:手动 clone 方法(如上所示)
优点:完全可控,能处理任何自定义逻辑(比如克隆时生成新的 id)。 缺点:每个类都要手动实现,属性多的时候容易遗漏。
方案二:structuredClone(现代浏览器 + Node.js 17+,首个 LTS 支持版本为 Node.js 18)
const cloned = structuredClone(originalObject);
优点:一行搞定,支持循环引用。
缺点:不支持函数、不会复制 Symbol 属性键、不支持 Element/Node 等 DOM 节点(但 File、Blob、ImageBitmap 等是支持的);丢失类的原型链(克隆出来的是普通对象,不是类实例)。
方案三:JSON 序列化反序列化
const cloned = JSON.parse(JSON.stringify(original));
优点:简单粗暴。 缺点:不支持 Date(会变成字符串)、undefined(会丢失)、函数、循环引用等。
在原型模式的语境下,推荐方案一——手动 clone 方法。因为原型模式的精髓不仅是”复制一个对象”,还在于”让每个对象自己决定怎么被复制”。你可能在克隆时需要做一些特殊处理,比如为克隆出的问卷生成新的 ID:
class Survey implements Cloneable<Survey> {
clone(): Survey {
const cloned = new Survey(
this.title,
this.description,
this.questions.map(q => q.clone()),
this.style.clone()
);
// 克隆时的特殊逻辑:新问卷需要新 ID
cloned.questions.forEach(q => {
q.id = generateUniqueId();
});
return cloned;
}
}
2.4 原型注册表:管理可复用的模板
当系统中有多个常用模板时,可以用一个注册表来管理它们:
class SurveyTemplateRegistry {
private templates: Map<string, Survey> = new Map();
register(name: string, template: Survey): void {
this.templates.set(name, template);
}
createFromTemplate(name: string): Survey {
const template = this.templates.get(name);
if (!template) {
throw new Error(`模板 "${name}" 不存在`);
}
return template.clone();
}
listTemplates(): string[] {
return Array.from(this.templates.keys());
}
}
// 注册预置模板
const registry = new SurveyTemplateRegistry();
registry.register('satisfaction', template);
registry.register('nps', npsTemplate);
registry.register('employee-engagement', engagementTemplate);
// 从模板创建新问卷
const mySurvey = registry.createFromTemplate('satisfaction');
mySurvey.title = '2026 Q1 客户满意度调查';
🤔 想一想 React 中的
React.cloneElement和 JavaScript 的Object.create与原型模式有什么关系?它们各自在什么程度上实现了”原型”的思想?
三、创建型模式全景总结
至此,创建型模式的五位成员全部登场。让我们站在更高的视角来看它们的定位:
| 模式 | 核心问题 | 一句话总结 | 真实场景 |
|---|---|---|---|
| 单例 | 只能有一个实例 | 全局唯一,统一访问 | 配置管理、日志、连接池 |
| 工厂方法 | 创建哪个类由子类决定 | 延迟决策,多态创建 | 跨平台通知、日志输出 |
| 抽象工厂 | 一族相关对象一起创建 | 保证产品族一致性 | UI 主题系统、数据库驱动 |
| 建造者 | 复杂对象分步构造 | 拆解步骤,灵活组装 | HTTP 请求、SQL 查询、配置对象 |
| 原型 | 复制已有对象创建新对象 | 克隆模板,按需修改 | 问卷模板、文档模板、游戏存档 |
选择哪种创建型模式,可以问自己几个问题:
- 需要全局唯一吗? → 单例
- 创建过程简单,但需要根据条件选择不同的类? → 工厂方法/抽象工厂
- 创建过程复杂,需要分步骤组装? → 建造者
- 已有一个类似的对象,想在此基础上修改? → 原型
下一章,我们进入结构型模式的世界——从”怎么创建对象”转向”怎么组合对象”。
📝 结尾自测
- 建造者模式解决的核心问题是什么?它和”传入配置对象”有什么本质区别?
- Director(指挥者)角色在建造者模式中的作用是什么?什么时候需要它?
- 原型模式为什么强调”深拷贝”?浅拷贝会导致什么问题?
structuredClone、JSON.parse(JSON.stringify(...))、手动 clone 方法各有什么优缺点?- 如果让你设计一个”邮件模板系统”,用户可以基于预设模板快速创建自定义邮件,你会选择哪种创建型模式?请说明理由。
购买课程解锁全部内容
写出优雅代码:10 章掌握 23 种设计模式
¥29.90