工厂模式
把对象的创建过程封装起来,使用方只关心"要什么",不关心"怎么造"。
工厂模式解决的痛点是:new 一个具体类会把调用方和这个类焊死。一旦要换实现、要按环境挑实现、要在创建前后做统一处理,就必须修改所有调用点。
识别信号:代码里出现了根据条件 new 不同类的 if/else,而且这个分支散落在多个地方。
一、简单工厂(Simple Factory)
严格说它不属于 GoF 的 23 种模式,但最常用:用一个函数封装创建逻辑,按参数返回不同实例。
type Career = 'coder' | 'hr' | 'driver' | 'boss';
interface Employee {
careerName: string;
work: string[];
}
const CAREER_MAP: Record<Career, () => Employee> = {
coder: () => ({ careerName: '程序员', work: ['写代码', '修 Bug'] }),
hr: () => ({ careerName: 'HR', work: ['招聘', '员工信息管理'] }),
driver: () => ({ careerName: '司机', work: ['开车'] }),
boss: () => ({ careerName: '老板', work: ['喝茶', '开会', '审批文件'] }),
};
function createEmployee(career: Career): Employee {
const creator = CAREER_MAP[career];
if (!creator) throw new Error(`未知职业: ${career}`);
return creator();
}
createEmployee('coder'); // { careerName: '程序员', work: [...] }
为什么用查找表而不是 switch
switch 每加一种职业就要改 createEmployee 函数体,违反开放封闭原则;查找表可以在别处扩展:
CAREER_MAP.designer = () => ({ careerName: '设计师', work: ['画图'] });
另外 TS 的 Record<Career, ...> 会在漏写某个职业时编译报错,比 switch 的 default 分支更早发现问题。
关于"构造函数里 return 对象"的老写法
一些教程里会写:
function Factory(career) {
if (this instanceof Factory) return new this[career]();
return new Factory(career);
}
Factory.prototype = { coder: function () { /* ... */ } };
它利用了"构造函数返回对象时会覆盖 this"这个特性,能跑通,但:把职业构造函数挂在 prototype 上、混用 new 与非 new 调用、返回值类型无法被 TS 推断——可读性和可维护性都很差,不建议在新代码里使用。
简单工厂的局限:所有产品的创建逻辑集中在一个函数里。产品种类多、创建逻辑复杂时,这个函数会膨胀成一坨。
二、工厂方法(Factory Method)
定义一个创建对象的接口,让子类决定实例化哪一个类。
把"选哪个产品"的决定权从工厂函数下放到子类,工厂本身对修改封闭。
abstract class Dialog {
// 模板方法:固定的流程
render() {
const btn = this.createButton(); // 具体造哪种按钮,交给子类
btn.onClick(() => console.log('closed'));
return btn.render();
}
// 工厂方法:由子类实现
protected abstract createButton(): Button;
}
interface Button {
render(): string;
onClick(cb: () => void): void;
}
class WebDialog extends Dialog {
protected createButton(): Button { return new HTMLButton(); }
}
class MobileDialog extends Dialog {
protected createButton(): Button { return new NativeButton(); }
}
调用方只依赖 Dialog 这个抽象,新增一种平台只需要加一个子类,不改任何已有代码。
三、抽象工厂(Abstract Factory)
提供一个接口,用于创建一系列「相关」的对象,而无需指定它们的具体类。
与工厂方法的区别:工厂方法针对一个产品,抽象工厂针对一族产品,并保证它们互相搭配。
典型场景是换肤 / 多主题 / 多平台组件库:
interface UIFactory {
createButton(): Button;
createInput(): Input;
createModal(): Modal;
}
class LightThemeFactory implements UIFactory {
createButton() { return new LightButton(); }
createInput() { return new LightInput(); }
createModal() { return new LightModal(); }
}
class DarkThemeFactory implements UIFactory {
createButton() { return new DarkButton(); }
createInput() { return new DarkInput(); }
createModal() { return new DarkModal(); }
}
// 业务代码只认 UIFactory,切换主题只换这一行
function renderApp(factory: UIFactory) {
factory.createButton().render();
factory.createInput().render();
}
renderApp(isDark ? new DarkThemeFactory() : new LightThemeFactory());
抽象工厂的核心价值是保证同族产品不会被混搭(不会出现深色按钮配浅色输入框)。代价是新增一种产品(比如 createTable)要改所有工厂类——它对"新增产品族"开放,对"新增产品种类"封闭。
四、建造者模式(Builder)
工厂关心"造哪个",建造者关心"分步骤造一个复杂对象"。
识别信号:构造函数参数超过 4 个,或者存在大量可选参数。
class QueryBuilder {
private table = '';
private wheres: string[] = [];
private orders: string[] = [];
private limitNum?: number;
from(table: string) { this.table = table; return this; } // 返回 this 实现链式调用
where(cond: string) { this.wheres.push(cond); return this; }
orderBy(col: string, dir: 'ASC' | 'DESC' = 'ASC') {
this.orders.push(`${col} ${dir}`); return this;
}
limit(n: number) { this.limitNum = n; return this; }
build(): string {
if (!this.table) throw new Error('必须指定表名'); // 在 build 时统一校验
let sql = `SELECT * FROM ${this.table}`;
if (this.wheres.length) sql += ` WHERE ${this.wheres.join(' AND ')}`;
if (this.orders.length) sql += ` ORDER BY ${this.orders.join(', ')}`;
if (this.limitNum != null) sql += ` LIMIT ${this.limitNum}`;
return sql;
}
}
new QueryBuilder().from('users').where('age > 18').orderBy('id', 'DESC').limit(10).build();
// SELECT * FROM users WHERE age > 18 ORDER BY id DESC LIMIT 10
前端里到处都是它:axios 的实例配置、ECharts 的 option 组装、Koa 的中间件链、各种测试库的断言链。
JS 中的简化版:配置对象
参数多但不需要分步骤构建时,直接用配置对象 + 默认值就够了,不必上 Builder:
function createChart({ type = 'line', width = 300, height = 200, ...rest } = {}) { /* ... */ }
Builder 真正的优势在于:中间状态可以传递、可以条件式地添加步骤(if (keyword) qb.where(...))。
五、几种工厂怎么选
| 需求 | 用哪个 |
|---|---|
就是想把 new 藏起来,产品种类固定且少 | 简单工厂(一个函数 + 查找表) |
| 产品会持续新增,希望不改已有代码 | 工厂方法 |
| 要成套创建、且必须保证同族搭配 | 抽象工厂 |
| 单个对象构造过程复杂、可选项多 | 建造者 |
不要为了模式而模式
前端 90% 的场景,一个返回对象字面量的普通函数就是最好的工厂:
const createUser = (name, role = 'guest') => ({ name, role, createdAt: Date.now() });
只有当"选择哪个实现"这件事本身开始变复杂时,才需要往上加层。
下一步 👉 代理模式
