讲解
买电脑整机时,你不会自己攒 CPU、主板、内存的混搭——品牌整机帮你保证这套配件是互相兼容的「一整套」。抽象工厂模式解决的就是这类问题:要创建的不是单个对象,而是一族相互关联、必须配套的对象,并且要保证客户端不会把不同族的产品混着用。
GoF 的定义:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。结构上,抽象工厂是一个接口,声明了一组「创建产品 A、创建产品 B……」的方法;每个具体工厂对应一个「产品族」(比如 Windows 风格一整套 UI 组件、Mac 风格一整套),客户端只面向抽象工厂编程,换成另一个族时整体替换工厂即可。
它和工厂方法的关系可以用一句话概括:工厂方法是「一个工厂造一种产品」,抽象工厂是「一个工厂造一族配套产品」。很多抽象工厂的实现里,每个创建方法本身就是一个工厂方法。选择标准也清晰:产品之间没有配套约束时用工厂方法,有「必须同一族」约束时用抽象工厂。
典型适用场景:跨平台 UI 组件库(按钮、复选框、下拉框必须同一风格)、数据库访问层(连接、命令、事务必须来自同一驱动)、主题系统(一套配色、字体、间距成套切换)。它的软肋同样明显:产品族里新增一种产品(比如所有 UI 族都要加「开关」组件)时,抽象接口和每个具体工厂都要加方法——对产品种类是封闭的,对产品族是开放的。
示例
跨平台 UI 组件族:UIFactory 一次产出一套风格一致的按钮和复选框,客户端 buildDialog 完全不知道自己在哪个平台上:
import assert from 'node:assert/strict';
interface Button {
render(): string;
}
interface Checkbox {
render(): string;
}
class WinButton implements Button {
render(): string {
return '[Win 按钮]';
}
}
class WinCheckbox implements Checkbox {
render(): string {
return '[Win 复选框]';
}
}
class MacButton implements Button {
render(): string {
return '(Mac 按钮)';
}
}
class MacCheckbox implements Checkbox {
render(): string {
return '(Mac 复选框)';
}
}
// 抽象工厂:一次创建一整套风格一致的产品族
interface UIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
class WinFactory implements UIFactory {
createButton(): Button {
return new WinButton();
}
createCheckbox(): Checkbox {
return new WinCheckbox();
}
}
class MacFactory implements UIFactory {
createButton(): Button {
return new MacButton();
}
createCheckbox(): Checkbox {
return new MacCheckbox();
}
}
// 客户端只面向抽象工厂,不可能混搭出「Win 按钮 + Mac 复选框」
function buildDialog(factory: UIFactory): string {
return factory.createButton().render() + ' + ' + factory.createCheckbox().render();
}
assert.equal(buildDialog(new WinFactory()), '[Win 按钮] + [Win 复选框]');
assert.equal(buildDialog(new MacFactory()), '(Mac 按钮) + (Mac 复选框)');
console.log(buildDialog(new WinFactory()));
console.log(buildDialog(new MacFactory()));
关键收益在类型系统的帮助下被放大了:buildDialog 的参数类型是 UIFactory,它拿到的按钮和复选框必然来自同一个工厂实例,「混搭出错」这一类 bug 在结构上被消灭了。切换平台只是换一个工厂实例,调用方代码零改动。
常见坑
- 给不相关的产品也套抽象工厂:产品之间没有「必须配套」约束时,这个模式纯属增加间接层,用工厂方法甚至直接 new 即可。
- 低估「新增产品种类」的成本:产品族里加一种产品要动抽象接口和所有具体工厂,产品种类频繁膨胀的领域要慎重。
- 客户端绕过工厂直接 new 具体产品:这等于拆掉了模式的全部保护,团队约定上应禁止在具体工厂之外出现产品类的 new。
- 和建造者模式混淆:抽象工厂强调「一族配套产品、立即返回」;建造者强调「分步骤装配一个复杂对象」。后面建造者一章会再对比。
- 工厂实例满天飞:一个运行上下文通常只需要一个工厂实例,入口选定后通过依赖注入往下传,而不是到处 new WinFactory()。
小结
抽象工厂面向「一族配套产品」的创建,保证客户端不会混搭不同族;它对「换族」开放、对「加产品种类」封闭。下一章看创建型里最出名也最容易被滥用的一个:单例模式。