工厂方法解决"一类产品的创建",抽象工厂解决"一族相关产品的创建"。听起来抽象,但落地很具体:你做跨平台 GUI 时,Mac 上要 Mac 风格的 Button、TextField、Checkbox,Windows 上要 Windows 风格的全部对应控件 —— 这些控件必须"配对",混用就是灾难。抽象工厂就是为这种"产品族要保持一致"的场景设计的。这篇文章讲透它的结构、典型用法和与工厂方法的边界。
从一个具体问题切入
假设你要做一个跨平台的对话框框架,支持 Mac / Windows / Linux 三种风格。每种风格都有自己的按钮、文本框、复选框。最朴素的做法:
public class Dialog {
public void render(String os) {
if (os.equals("mac")) {
new MacButton().render();
new MacTextField().render();
new MacCheckbox().render();
} else if (os.equals("windows")) {
new WindowsButton().render();
new WindowsTextField().render();
new WindowsCheckbox().render();
} else {
new LinuxButton().render();
new LinuxTextField().render();
new LinuxCheckbox().render();
}
}
}
问题清单:
- 每加一个平台,要改
render全部分支;每加一种控件,要在所有分支里加一行。 - 同平台的控件可能写错配对(把 MacButton 和 WindowsTextField 放一起,样式就乱了)。
- 这堆 if/else 散布在十几个不同地方,改起来九死一生。
抽象工厂解决思路:把"一个平台的所有控件"打包到一个工厂里,保证它们配套出厂。
标准结构
// 1. 一组抽象产品
interface Button { void render(); }
interface TextField { void render(); }
interface Checkbox { void render(); }
// 2. 具体产品(每个平台一组)
class MacButton implements Button { public void render() { System.out.println("Mac 按钮"); } }
class MacTextField implements TextField { public void render() { System.out.println("Mac 文本框"); } }
class MacCheckbox implements Checkbox { public void render() { System.out.println("Mac 复选框"); } }
class WindowsButton implements Button { public void render() { System.out.println("Win 按钮"); } }
class WindowsTextField implements TextField { public void render() { System.out.println("Win 文本框"); } }
class WindowsCheckbox implements Checkbox { public void render() { System.out.println("Win 复选框"); } }
// 3. 抽象工厂:列出所有创建方法
interface UIFactory {
Button createButton();
TextField createTextField();
Checkbox createCheckbox();
}
// 4. 具体工厂:每个平台一个
class MacFactory implements UIFactory {
public Button createButton() { return new MacButton(); }
public TextField createTextField() { return new MacTextField(); }
public Checkbox createCheckbox() { return new MacCheckbox(); }
}
class WindowsFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public TextField createTextField() { return new WindowsTextField(); }
public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
// 5. 使用方依赖抽象工厂,不知道具体平台
class Dialog {
private final UIFactory factory;
public Dialog(UIFactory factory) { this.factory = factory; }
public void render() {
factory.createButton().render();
factory.createTextField().render();
factory.createCheckbox().render();
}
}
// 6. 启动时挑一个具体工厂
UIFactory factory = isMac() ? new MacFactory() : new WindowsFactory();
new Dialog(factory).render();
结果:Dialog 类对"操作系统"零感知 —— 它只知道有一个 UIFactory 能造控件。换平台只需启动期换一个工厂,Dialog 一行不用改。
工厂方法 vs 抽象工厂
关键区别看下表:
工厂方法 抽象工厂
解决问题 单个产品的创建变化 一族相关产品要保持一致
扩展维度 一个 两个(产品类型 × 平台)
继承结构 一个抽象创建者 一个抽象工厂,多个抽象产品
新加产品 新增一个工厂子类 所有具体工厂都要新增一个方法
新加平台 - 新增一个具体工厂(实现所有方法)
记忆口诀:"工厂方法是一维扩展,抽象工厂是二维矩阵"。当你画出来发现产品是一张 N×M 的表(N 个产品 × M 个平台/主题/方言),那就是抽象工厂的舞台。
实战:跨数据库 ORM 方言
抽象工厂在 ORM 框架里非常常见。Hibernate 的 Dialect、MyBatis 的 DatabaseIdProvider、Django 的 database backend 都是抽象工厂的变体。
// 抽象产品:每种数据库需要的不同 SQL 生成器
interface SqlGenerator {
String generateLimit(int offset, int limit);
String generateBoolType();
String quote(String identifier);
}
// 抽象工厂
interface DialectFactory {
SqlGenerator createSqlGenerator();
TypeConverter createTypeConverter();
Reserve createReserve(); // 关键字
}
// 具体:MySQL
class MySqlDialect implements DialectFactory {
public SqlGenerator createSqlGenerator() {
return new SqlGenerator() {
public String generateLimit(int o, int l) { return "LIMIT " + o + ", " + l; }
public String generateBoolType() { return "TINYINT(1)"; }
public String quote(String i) { return "`" + i + "`"; }
};
}
// ... 其他
}
// 具体:PostgreSQL
class PostgresDialect implements DialectFactory {
public SqlGenerator createSqlGenerator() {
return new SqlGenerator() {
public String generateLimit(int o, int l) { return "LIMIT " + l + " OFFSET " + o; }
public String generateBoolType() { return "BOOLEAN"; }
public String quote(String i) { return "\"" + i + "\""; }
};
}
}
// 具体:Oracle
class OracleDialect implements DialectFactory {
public SqlGenerator createSqlGenerator() {
return new SqlGenerator() {
// Oracle 没有 LIMIT,要用 ROWNUM 或 ROW_NUMBER()
public String generateLimit(int o, int l) {
return "WHERE ROWNUM > " + o + " AND ROWNUM <= " + (o + l);
}
public String generateBoolType() { return "NUMBER(1)"; }
public String quote(String i) { return "\"" + i + "\""; }
};
}
}
用法上,框架启动时根据连接字符串选一次方言,后续所有 SQL 生成、类型转换都通过这一个工厂,保证"一个查询里的所有 SQL 片段都是同一种数据库方言"。
实战:不同主题的图表组件
前端常见场景:同一套图表组件,要支持 light / dark / 高对比度三套主题。每个主题里 colors、fonts、axisStyles、tooltipStyles 都不同,且必须配对。
// 用 TypeScript
interface ColorPalette { primary: string; bg: string; text: string; grid: string; }
interface FontSet { family: string; sizeBase: number; }
interface AxisStyle { tick: string; line: string; }
interface ThemeFactory {
createColors(): ColorPalette;
createFonts(): FontSet;
createAxis(): AxisStyle;
}
const lightTheme: ThemeFactory = {
createColors: () => ({ primary: '#0ea5e9', bg: '#fff', text: '#111', grid: '#eee' }),
createFonts: () => ({ family: 'Inter', sizeBase: 14 }),
createAxis: () => ({ tick: '#999', line: '#ccc' }),
};
const darkTheme: ThemeFactory = {
createColors: () => ({ primary: '#38bdf8', bg: '#0f172a', text: '#f1f5f9', grid: '#334155' }),
createFonts: () => ({ family: 'Inter', sizeBase: 14 }),
createAxis: () => ({ tick: '#94a3b8', line: '#475569' }),
};
class Chart {
constructor(private theme: ThemeFactory) {}
render() {
const colors = this.theme.createColors();
const fonts = this.theme.createFonts();
const axis = this.theme.createAxis();
// 用这些"配套"参数渲染图表 —— 永远不会出现"light 的字 + dark 的背景"
}
}
new Chart(darkTheme).render();
函数式表达:Map of Factories
在动态语言里,抽象工厂常常简化为"一个映射"。Python 例:
themes = {
'light': {
'colors': lambda: {'primary': '#0ea5e9', 'bg': '#fff'},
'fonts': lambda: {'family': 'Inter'},
},
'dark': {
'colors': lambda: {'primary': '#38bdf8', 'bg': '#0f172a'},
'fonts': lambda: {'family': 'Inter'},
},
}
def render_chart(theme_name):
theme = themes[theme_name]
colors = theme['colors']()
fonts = theme['fonts']()
# ...
这种"用 dict 代替接口 + 类"的写法在 Python / JS 里更地道。本质上还是抽象工厂,但视觉上轻量得多。
抽象工厂的代价
抽象工厂解决"一族产品一致性",代价也大:
1. 加新产品很贵
原本只支持 Button / TextField / Checkbox 三个控件,现在要加 Slider。所有具体工厂都要加一个 createSlider 方法。如果你有 Mac / Windows / Linux / iOS / Android 五个工厂,这是五处改动 —— 而且少改一个就编译错。
这也是抽象工厂被批评的"产品扩展困难"。所以选用前一定要评估:"产品数量是不是相对稳定?" 是的话上抽象工厂;不是的话考虑其他方案(比如组件注册表)。
2. 容易变成"上帝接口"
当具体工厂要提供 20 个 create 方法时,接口本身就很重。再加上每个平台一份实现,类数量爆炸。可以用"组合多个小工厂"来减负:
// 把 UIFactory 拆成三个小工厂,Dialog 注入这三个
interface InputFactory { Button createButton(); Checkbox createCheckbox(); }
interface TextFactory { TextField createTextField(); Label createLabel(); }
interface ChromeFactory { Window createWindow(); MenuBar createMenuBar(); }
class Dialog {
public Dialog(InputFactory inputs, TextFactory texts, ChromeFactory chrome) { ... }
}
抽象工厂与依赖注入
Spring 等 DI 容器经常被用来"自动选具体工厂":
// 配置文件决定用哪个工厂
@Profile("mac")
@Component
public class MacFactory implements UIFactory { ... }
@Profile("windows")
@Component
public class WindowsFactory implements UIFactory { ... }
// 用方
@Component
public class Dialog {
private final UIFactory factory;
public Dialog(UIFactory factory) { this.factory = factory; }
}
// 启动时
java -Dspring.profiles.active=mac -jar app.jar
容器接管了"挑工厂"这件事,代码里看不到任何 if/else,极其干净。
识别抽象工厂场景的关键问题
遇到一个设计抉择,问下面三个问题判断是不是抽象工厂:
- 有没有"一族"对象需要一起创建?(不只是一个)
- 这些对象之间有"配对约束"吗?(Mac 控件不能和 Win 控件混用)
- "平台/主题/方言"的数量是有限且稳定的吗?(2-5 个,不是开放扩展)
三个 yes 就上抽象工厂。某个 no,看具体哪个:
- "族"只有一个产品?改用工厂方法。
- 没有配对约束?直接 new 或简单工厂就够。
- 平台数量会无限扩展?改用"插件 + 注册表"模式。
插件 + 注册表:抽象工厂的现代替代
当"平台/方言"数量可能无限增长时,抽象工厂的"每加一个就要写一个完整工厂类"成本太高。这时常见的替代是"插件 + 注册表":
class DialectRegistry {
private static final Map<String, Supplier<Dialect>> map = new HashMap<>();
public static void register(String name, Supplier<Dialect> factory) {
map.put(name, factory);
}
public static Dialect get(String name) {
return map.get(name).get();
}
}
// 各方言"自己"在启动期注册,核心代码不需要改
public class CustomNoSQLDialectModule {
static {
DialectRegistry.register("mongo", MongoDialect::new);
}
}
这种"反向控制"让框架核心永远不需要改 —— 加新方言就是新加一个 jar 包或模块,在自己的初始化里注册自己。Java 的 ServiceLoader、Spring 的 SPI 都是这套思路。
写在最后
抽象工厂模式像一把锋利但重的刀:用对场景威力很大(避免产品族错配 + 平台切换零侵入),用错场景就是过度设计。判断它是不是"对"的标志,看你画出来的"产品 × 平台"矩阵是不是真的填满了 —— 矩阵稀疏(很多格子用不到)时,抽象工厂就显得笨重,改用工厂方法 + 简单工厂的组合更合适。
给一个落地建议:第一次发现"我要为 Mac / Windows 分别写一套实现"时,先用工厂方法;当第二次、第三次发现"还有一组组件要按平台区分"时,把它们提升为抽象工厂。设计模式不是预先用,而是被代码"逼"出来的 —— 重复出现的相似结构,自然会浮现到合适的抽象层级。
—— 别看了 · 2026