工厂模式

把对象的创建过程封装起来,使用方只关心"要什么",不关心"怎么造"。

工厂模式解决的痛点是: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() });

只有当"选择哪个实现"这件事本身开始变复杂时,才需要往上加层。


下一步 👉 代理模式

上次更新:
贡献者: Joe