工厂模式不是语法糖,是接线盒

0 阅读6分钟

你有没有写过这种代码:在三个不同的页面里,分别用 if/else 判断当前环境,然后创建不同的 service 实例?

第一次写的时候没觉得有问题。等第四个页面也需要同样的判断,你才意识到:这些分支逻辑散落在四个地方,改一个漏三个。

这就是工厂模式要解决的核心问题——不是"怎么创建对象",而是 "谁来管创建对象时的分支判断"

一、工厂模式到底在做什么

patterns.dev 上对工厂模式的定义很简洁:一个函数返回新对象,不使用 new 关键字。

const createUser = ({ firstName, lastName, email }) => ({
  firstName,
  lastName,
  email,
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
});

看起来平平无奇——就是个返回对象的函数嘛。

但"平平无奇"恰恰是重点。它的价值不在语法本身,而在于它提供了一个受控的创建边界。当你的系统需要根据不同条件创建不同实现时,工厂就是那个唯一的"接线盒":所有创建分支汇聚于此,业务代码只管拿到结果,不关心里面接了哪根线。

工厂的核心价值不是简化创建,是集中管理创建时的分支判断。

二、没有工厂时,分支散落成什么样

想象一个前端项目需要根据运行环境(开发 / 测试 / 生产)使用不同的 API service:

// ❌ 页面 A
const api = env === 'dev' ? new MockApi() : new RealApi();

// ❌ 页面 B(复制粘贴了一份)
const api = env === 'dev' ? new MockApi() : new RealApi();

// ❌ 页面 C(又复制了一份,还漏了 test 环境)
const api = env === 'dev' ? new MockApi() : new RealApi();

三个页面,三份一模一样的判断逻辑。有人加了 staging 环境?改三个地方。有人漏改一个?线上出 bug。

用工厂收拢之后:

// ✅ 工厂:所有分支只在这里
function createApiService(env) {
  switch (env) {
    case 'dev':     return new MockApi();
    case 'test':    return new MockApi({ delay: 0 });
    case 'staging': return new StagingApi();
    default:        return new RealApi();
  }
}

// 页面 A / B / C 都只写一行
const api = createApiService(currentEnv);

分支逻辑从"散落在 N 个消费方"变成"集中在 1 个工厂"。加环境、改逻辑,只动一个地方。

这就像大型仓储的分拣中心——车间不会自己跑到仓库翻零件,分拣中心统一接单、按需配送。工厂就是代码世界的分拣中心:内部调整分配规则,车间(业务代码)完全不用改。

三、前端最常见的四种工厂场景

工厂模式不是后端专利。前端项目里,以下四种场景几乎一定会遇到:

场景工厂负责什么典型例子
组件工厂根据类型/配置返回不同组件动态表单:text → TextField,select → SelectField
Service 工厂根据环境/平台返回不同服务实现开发用 MockApi,生产用 RealApi
Adapter 工厂根据第三方依赖返回统一接口的适配器高德地图 / Google Maps 统一渲染接口
Tool Client 工厂根据配置返回不同工具链客户端AI agent pipeline 中按模型名返回不同 LLM client

组件工厂在 React 中特别常见。与其在 JSX 里写一堆 if/else,不如用一张映射表充当工厂:

const FIELD_MAP = {
  text:     TextField,
  select:   SelectField,
  checkbox: CheckboxField,
};

function createField(type, props) {
  const Component = FIELD_MAP[type] || TextField;
  return <Component {...props} />;
}

这张映射表就是最轻量的工厂——加新类型只需加一行映射,不改任何业务代码。

工厂的扩展方式是"加一行注册",而不是"改 N 处 if/else"。

四、工厂函数 vs class:一笔 trade-off 账

patterns.dev 原文特别强调了一个常被忽略的区别:内存效率

工厂函数每次调用都返回一个全新对象,对象上的方法是各自独立的副本

const u1 = createUser({ firstName: 'A', lastName: 'B', email: 'a@b.com' });
const u2 = createUser({ firstName: 'C', lastName: 'D', email: 'c@d.com' });
u1.fullName === u2.fullName; // false — 两份不同的函数

而 class 的方法存在 prototype 上,所有实例共享同一份引用

const u3 = new User('A', 'B', 'a@b.com');
const u4 = new User('C', 'D', 'c@d.com');
u3.fullName === u4.fullName; // true — 原型上同一个函数

创建 10 个对象感受不到差异。创建 10000 个,内存差距就出来了。

但反过来,工厂函数也有 class 给不了的东西:

维度工厂函数class
内存效率❌ 每实例复制方法✅ 原型共享
this 安全✅ 闭包捕获,不怕丢失❌ 解构/回调中 this 易丢
封装能力✅ 闭包天然私有✅ # 私有字段(较新)
组合灵活性✅ 自由混入⚠️ 单继承限制
instanceof❌ 不支持✅ 支持

经济学有个核心概念叫机会成本——你选择 A 的代价就是放弃 B。工厂函数用内存换来了 this 安全和封装便利;class 用 this 的心智负担换来了内存效率。

好的工程决策不是选"最好的",是选"在当前约束下机会成本最低的"。

实际判断很简单:

• 轻量对象、数量有限、需要传回调 → 工厂函数

• 重方法复用、大量实例、需要继承体系 → class

• React 组件工厂 → 映射表 + 函数(最符合 React 的函数式风格)

五、工厂模式的边界在哪

工厂不是万能药。两种场景不需要它:

场景一:类型固定且少。  只有两种按钮(主要 / 次要),直接 isPrimary ? <Primary /> : <Secondary /> 就行。为两个分支建工厂是过度设计。

场景二:创建逻辑本身就很简单。  new User(name, email) 没有任何分支判断,套一层 createUser 只增加了一层无意义的间接性。

patterns.dev 原文也坦率地说:在 JavaScript 中,工厂模式本质上"just a function that returns an object"——如果没有分支判断需要集中管理,它就只是一个普通函数,没有额外的设计价值。

判断是否需要工厂的信号很明确:

信号该怎么做
同样的 if/else 创建逻辑出现在 ≥ 2 处✅ 收拢到工厂
创建时需要根据环境/配置/类型做分支✅ 工厂是天然边界
只有 1-2 个固定类型,无扩展预期❌ 直接条件表达式
创建逻辑无分支,只是 new Xxx()❌ 不需要工厂

这和家里的电器插座一样——你不需要为每个电器都配一个转接头工厂。但如果你要带一箱电器去五个国家,一个能根据国家参数自动匹配制式的转接头工厂,就是刚需。

六、工厂在架构决策链中的位置

如果把最近几篇文章串起来,你会发现工厂模式刚好填补了一个缺口:

原则 / 模式回答什么问题
SoC(关注点分离)这段逻辑应该放在哪一层?
SRP(单一职责)这个模块应该只干一件事
DI / DIP(依赖反转)依赖应该指向谁?
AHA(避免草率抽象)什么时候该提炼抽象?
Factory(工厂模式)"创建不同实现"这个分支判断,应该由谁管?

SoC 和 SRP 告诉你把创建逻辑从业务逻辑里分出来;DI 告诉你消费方不该直接依赖具体实现;AHA 告诉你别急着抽象——先确认分支确实在多处重复。

工厂是这条决策链的最后一环:当你确认了边界、方向、时机,工厂就是那个把"创建谁"集中到一处的接线盒。

如果你只想带走一句话,我建议记这个:

工厂的价值不是让创建更简洁,而是让"创建时的分支判断"只存在于一个地方。散落的 if/else 是定时炸弹,集中的工厂是保险丝。

参考原文:

• patterns.dev — Factory Pattern

qrcode_for_gh_6a9e7f3719d6_344.jpg