跳到主要内容
预计阅读 28 分钟

创建型模式(下)—— 建造者模式、原型模式

有些对象不是一步就能造好的,它像一道工序繁多的菜品,需要一步步组装;还有些对象的创建成本很高,直接”克隆”一份比从头构建要划算得多。建造者模式和原型模式分别应对这两类场景。

📋 开篇自测:你已经知道多少?

  1. TypeScript 中构造函数参数超过 5 个时,你通常如何处理?
  2. JavaScript 的 Object.assignstructuredClone 分别实现的是什么层级的拷贝?
  3. 你能说出”建造者模式”和”工厂模式”的核心区别吗?

一、建造者模式:分步构造复杂对象

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 节点(但 FileBlobImageBitmap 等是支持的);丢失类的原型链(克隆出来的是普通对象,不是类实例)。

方案三: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 查询、配置对象
原型复制已有对象创建新对象克隆模板,按需修改问卷模板、文档模板、游戏存档

选择哪种创建型模式,可以问自己几个问题:

  1. 需要全局唯一吗? → 单例
  2. 创建过程简单,但需要根据条件选择不同的类? → 工厂方法/抽象工厂
  3. 创建过程复杂,需要分步骤组装? → 建造者
  4. 已有一个类似的对象,想在此基础上修改? → 原型

下一章,我们进入结构型模式的世界——从”怎么创建对象”转向”怎么组合对象”。


📝 结尾自测

  1. 建造者模式解决的核心问题是什么?它和”传入配置对象”有什么本质区别?
  2. Director(指挥者)角色在建造者模式中的作用是什么?什么时候需要它?
  3. 原型模式为什么强调”深拷贝”?浅拷贝会导致什么问题?
  4. structuredCloneJSON.parse(JSON.stringify(...))、手动 clone 方法各有什么优缺点?
  5. 如果让你设计一个”邮件模板系统”,用户可以基于预设模板快速创建自定义邮件,你会选择哪种创建型模式?请说明理由。

购买课程解锁全部内容

写出优雅代码:10 章掌握 23 种设计模式

¥29.90