参数太多,构造函数快炸了?

0 阅读7分钟

你大概写过这种代码:一个函数接收十几个参数,其中大半是可选的,调用的时候一堆 null, undefined, false, null 排成一行,连你自己都看不清第 7 个参数到底控制什么。

这不是你的问题。当一个对象的构造需要大量可选配置时,问题不是"怎么把参数传进去",而是"怎么组织构建过程本身"。  Builder 模式解决的正是这件事。

一、两个真实的痛点

痛点 1:子类爆炸

假设你在写一个 House 组件。基础款只需要墙、门、窗、屋顶。但需求一来——有的要花园,有的要车库,有的要泳池,有的要地暖。

最直觉的做法是继承:HouseWithGarageHouseWithPoolHouseWithGarageAndPool……

每多一个可选特性,子类数量就翻倍。这在设计模式里叫子类爆炸

子类爆炸:每增加一个可选特性,子类数量翻倍增长

子类爆炸:每增加一个可选特性,子类数量翻倍增长

痛点 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 将构建过程拆成独立步骤

核心概念:三个角色

角色职责类比
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 MethodBuilder
核心问题创建哪种对象?这个对象怎么配
产品复杂度相对简单,一步创建复杂,多步骤、多配置
返回时机调用即返回构建完成后才返回
可选参数少,或通过子类处理多,通过步骤按需组合
典型场景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 模式完整结构:接口、具体建造者、产品、指挥者

Builder 模式完整结构:接口、具体建造者、产品、指挥者

再用一张表理清这几个角色的协作关系:

步骤谁做做什么
1Client创建 Builder 实例
2Client(或 Director)调用一系列构建步骤
3Builder执行每个步骤,内部累积状态
4Client调用 build() / getProduct() 取走成品
5Builder返回产品,重置内部状态(准备下一次构建)

这就像宜家的组装流程:你买回一堆板材和螺丝(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)

qrcode_for_gh_6a9e7f3719d6_344.jpg