你大概写过这种代码:一个函数接收十几个参数,其中大半是可选的,调用的时候一堆 null, undefined, false, null 排成一行,连你自己都看不清第 7 个参数到底控制什么。
这不是你的问题。当一个对象的构造需要大量可选配置时,问题不是"怎么把参数传进去",而是"怎么组织构建过程本身"。 Builder 模式解决的正是这件事。
一、两个真实的痛点
痛点 1:子类爆炸
假设你在写一个 House 组件。基础款只需要墙、门、窗、屋顶。但需求一来——有的要花园,有的要车库,有的要泳池,有的要地暖。
最直觉的做法是继承:HouseWithGarage、HouseWithPool、HouseWithGarageAndPool……
每多一个可选特性,子类数量就翻倍。这在设计模式里叫子类爆炸。
子类爆炸:每增加一个可选特性,子类数量翻倍增长
痛点 2:参数爆炸(Telescoping Constructor)
你决定不搞继承了,改用一个"万能构造函数",把所有可能的配置都塞进参数列表:
new House(4, 2, true, false, true, null, false, 'wood', null)
第 5 个参数是什么?第 8 个呢?没人记得住。大部分参数在大部分场景下用不到,但你每次都得把它们填上。
万能构造函数:大量参数中只有少数真正需要
子类爆炸解决不了组合问题,参数爆炸解决不了可读性问题。 两条路都走不通的时候,Builder 登场了。
二、Builder 的核心思路
Builder 模式的解法出奇地简单:把构建过程拆成一系列独立的步骤,你只调用需要的那几步。
const house = new HouseBuilder()
.setWalls(4)
.setDoors(2)
.setGarage(true)
.setSwimmingPool(true)
.build()
和那串 null, undefined, false 比,哪个更清晰,一眼就看出来了。
这就像去餐厅点菜。Telescoping Constructor 让你面对 50 种食材自由搭配——自由度太高,反而不知道怎么选。Builder 模式相当于引入了菜单:你可以选基础套餐,也可以在套餐上单独加菜。参数太多时,问题不是"怎么传",而是"怎么组织"。
Builder 将构建过程拆成独立步骤
核心概念:三个角色
| 角色 | 职责 | 类比 |
|---|---|---|
| Builder | 提供每个构建步骤的具体实现 | 厨师——知道怎么做每道菜 |
| Director | 定义步骤的执行顺序(可选) | 菜单/套餐——决定上菜顺序 |
| Client | 选择 Builder、启动构建、取走产品 | 顾客——选套餐、加菜、买单 |
Director 不是必须的。 如果你的构建流程只有一两种固定组合,Client 直接调用 Builder 的步骤就够了。Director 的价值在于:当你有多种固定构建流程需要复用时,把它们封装起来。
三、前端最常见的 Builder 场景
你可能觉得 Builder 离前端很远,其实它就在你每天用的工具里。
场景 1:链式配置 API
// Axios 请求构建
const request = axios.create()
.defaults.baseURL = 'https://api.example.com'
// Webpack 配置(conceptually)
new ConfigBuilder()
.setEntry('./src/index.ts')
.setOutput({ path: './dist', filename: 'bundle.js' })
.addPlugin(new HtmlWebpackPlugin())
.addLoader({ test: /.tsx?$/, use: 'ts-loader' })
.build()
场景 2:测试数据构造
这是前端 Builder 模式最实用的场景之一。单测里需要构造各种变体的 mock 数据:
// ❌ 每次手写完整对象,冗余且难维护
const user = {
id: '1', name: 'Alice', email: 'alice@test.com',
role: 'admin', avatar: null, theme: 'dark',
notifications: true, language: 'zh-CN',
// ... 还有十几个字段
}
// ✅ Builder 模式:只声明你关心的差异
const user = new UserBuilder()
.withRole('admin')
.withTheme('dark')
.build() // 其余字段自动填充合理默认值
场景 3:复杂 UI 组件配置
// 图表库配置
const chart = new ChartBuilder()
.setType('line')
.setData(salesData)
.setXAxis({ label: '月份', format: 'MMM' })
.setYAxis({ label: '销售额', unit: '万元' })
.enableTooltip()
.enableZoom()
.build()
Builder 在前端的核心价值:把"一坨配置对象"变成"一串可读的构建步骤"。
四、Builder vs Factory,到底怎么选?
这是被问得最多的问题。很多人分不清 Builder 和 Factory Method 的界限,因为它们都是"创建型模式"。
一句话区分:Factory 关心"创建哪一类",Builder 关心"怎么一步步组装"。
| 维度 | Factory Method | Builder |
|---|---|---|
| 核心问题 | 创建哪种对象? | 这个对象怎么配? |
| 产品复杂度 | 相对简单,一步创建 | 复杂,多步骤、多配置 |
| 返回时机 | 调用即返回 | 构建完成后才返回 |
| 可选参数 | 少,或通过子类处理 | 多,通过步骤按需组合 |
| 典型场景 | createButton('primary') | new FormBuilder().addField().addValidation().build() |
| 类比 | 自动售货机——按按钮,出饮料 | 地铁三明治——选面包、选肉、选酱、烤不烤 |
判断清单
问自己三个问题:
对象有超过 5 个可选配置项吗? → 考虑 Builder
构建过程需要特定顺序吗? → 考虑 Builder + Director
只需根据类型创建不同实例? → Factory 就够了
Factory 是"选一个",Builder 是"攒一个"。
五、一个完整的 TypeScript 示例
看一个简化但完整的例子——构建 HTTP 请求配置:
interface RequestConfig {
url: string
method: string
headers: Record<string, string>
body?: unknown
timeout: number
retries: number
}
class RequestBuilder {
private config: Partial<RequestConfig> = {
method: 'GET',
headers: {},
timeout: 5000,
retries: 0,
}
setUrl(url: string) {
this.config.url = url
return this // 链式调用的关键
}
setMethod(method: string) {
this.config.method = method
return this
}
addHeader(key: string, value: string) {
this.config.headers![key] = value
return this
}
setBody(body: unknown) {
this.config.body = body
return this
}
setTimeout(ms: number) {
this.config.timeout = ms
return this
}
setRetries(n: number) {
this.config.retries = n
return this
}
build(): RequestConfig {
if (!this.config.url) {
throw new Error('URL is required')
}
return { ...this.config } as RequestConfig
}
}
// 使用
const config = new RequestBuilder()
.setUrl('/api/users')
.setMethod('POST')
.addHeader('Content-Type', 'application/json')
.setBody({ name: 'Alice' })
.setTimeout(3000)
.setRetries(2)
.build()
注意 build() 方法里的校验——Builder 模式的一个隐藏优势是:可以在最终构建时做完整性校验,防止产出"半成品"对象。 这比在构造函数里检查十几个参数的组合有效性,干净得多。
六、结构全景图
Builder 模式完整结构:接口、具体建造者、产品、指挥者
再用一张表理清这几个角色的协作关系:
| 步骤 | 谁做 | 做什么 |
|---|---|---|
| 1 | Client | 创建 Builder 实例 |
| 2 | Client(或 Director) | 调用一系列构建步骤 |
| 3 | Builder | 执行每个步骤,内部累积状态 |
| 4 | Client | 调用 build() / getProduct() 取走成品 |
| 5 | Builder | 返回产品,重置内部状态(准备下一次构建) |
这就像宜家的组装流程:你买回一堆板材和螺丝(Builder 提供的步骤),按说明书 A 装出书架,按说明书 B 装出电视柜。零件一样,组装顺序不同,产出完全不同。Builder 的核心不是零件本身,而是组装顺序的可编排性。
七、什么时候不该用 Builder?
设计模式最怕过度使用。Builder 的代价是多出一整套 Builder 类(接口 + 具体实现),如果对象本身很简单,这就是用大炮打蚊子。
不适合用 Builder 的信号
• 对象只有 2-3 个参数,且都是必填的
• 没有"可选配置"的概念
• 构建过程不存在顺序依赖
• 只需区分几种固定类型(用 Factory 更合适)
适合用 Builder 的信号
• 构造函数超过 5 个参数,其中大半可选
• 你需要用同一套步骤创建不同配置的对象
• 对象在构建完成前不应被使用(需要防半初始化)
• 代码里出现了大量 null, undefined, false 的参数占位
不是每个对象都需要 Builder,但每个被参数爆炸折磨过的开发者都应该知道它。
如果你只想带走一句话,我建议记这个:
Factory 解决"创建哪一类",Builder 解决"怎么一步步攒出来"。当参数多到你自己都看不懂调用代码时,就是 Builder 该出场的时候。
Builder 模式的本质不是什么高深的架构思想,它就是一种组织能力——把散落的配置参数,收编成一条清晰的构建流水线。用得好,你的代码读起来就像在说人话。
参考原文:
• Oleksandr Shvets — Builder (Refactoring.Guru)