你以为抽象工厂是「创建一组对象」------其实它是「锁定产品族兼容性」
抽象工厂(Abstract Factory)这个模式在 GoF 23 个里是被讲得最糊弄的一个。网上搜"抽象工厂",十篇文章里有八篇在画继承图:AbstractFactory → ConcreteFactory1 → ProductA1/ProductB1,然后一个 main 函数 demo 一下"如何切换产品族"。讲完就完事。
但真正在工程里用抽象工厂的人,踩的坑根本不是"怎么创建对象"。是怎么保证同一族产品的所有组件在同一个上下文里互相兼容------这事听起来废话,做起来全是坑。
我接手的第一个抽象工厂事故
2019 年我接手一个做企业 ERP 的项目。系统要支持多语言界面、中英日韩俄越之类的七八种语言,每种语言下还有浅色/深色两种主题。当时的架构师用了"看起来很优雅"的抽象工厂:
java
interface UIFactory {
Button createButton();
TextField createTextField();
Dialog createDialog();
Menu createMenu();
}
class ChineseLightUIFactory implements UIFactory { ... }
class ChineseDarkUIFactory implements UIFactory { ... }
class EnglishLightUIFactory implements UIFactory { ... }
// ... 等等
每种语言 × 每种主题 = 一个具体工厂。代码看起来很美好。
上线后第一个 bug 是:中文深色主题下,按钮的中文字体突然渲染成了一堆方块。原因是中文字体没专门做深色优化,工厂创建 TextField 时用了深色主题配套的西文字体,渲染中文直接挂了。
第二个 bug 更诡异:日文界面下,深色主题的菜单选中色偏黄,但浅色主题的菜单选中色偏白。原因是两套工厂的"菜单选中色"是从不同的 antd 主题配置里取的------浅色主题用的旧版本 antd,深色主题用的新版本。同一个 Dialog 在日文界面下的 Dialog 顶栏颜色和英文界面下的 Dialog 顶栏颜色都不一样。
抽象工厂掉进了"产品族兼容性"的坑。
抽象工厂的核心不是"创建",是"约束"
教科书告诉你抽象工厂"提供一个接口,用于创建相关或依赖对象的家族,而不需要指定具体类"。这句话里最关键的不是"创建",是"相关或依赖"。
工厂方法模式(Factory Method)解决的是"如何创建一个对象"的问题------你只需要一个 Button,用工厂方法就够了。
抽象工厂解决的问题是"如何保证一组对象在同一个上下文里互相兼容"------你需要 Button + TextField + Dialog + Menu 这四个一起出现,而且它们必须来自同一个"产品族",不能 Button 来自家族 A、TextField 来自家族 B。
所以抽象工厂的本质不是创建机制,是一致性约束机制。它通过把"一组必须配套出现的对象"的创建逻辑,封装在一个工厂里,强制保证你拿到的 Button 和 TextField 一定来自同一个家族。
我那个 ERP 项目的 bug,根源就在这里:架构师把"语言"和"主题"作为了抽象工厂的两个维度交叉切割(中文浅色/中文深色/英文浅色/英文深色),但实际上"中文深色主题"这个组合本身就不存在------中文主题根本就没做深色适配。"产品族"是"中文主题"本身(包含字体、配色、字间距、字数截断规则),而不是"语言 × 主题"的笛卡尔积。
错误的家族划分,让抽象工厂的"约束"完全失效------它不仅没强制一致性,反而让人误以为组合是合法的。
抽象工厂的正确打开方式
回头看那个项目,正确的做法是按"产品族"划分,而不是按"维度"划分:
java
// 一族产品 = 一套完整的、互相兼容的 UI
interface UIFamily {
Button createButton();
TextField createTextField();
Dialog createDialog();
Menu createMenu();
Theme getTheme();
}
// 不同产品族
class ChineseFamily implements UIFamily { ... } // 中文主题(不分浅色深色)
class MaterialYouFamily implements UIFamily { ... } // Material Design 主题
class FluentUIFamily implements UIFamily { ... } // 微软 Fluent Design 主题
每个产品族是一个完整的、自洽的产品集。家族的边界由"业务场景"决定,而不是由"维度组合"决定。
这个原则的工程意义是什么呢?当你发现一个产品族里某个组件不存在或不该存在时,你应该回到产品族定义,而不是去扩展工厂方法。
比如发现中文场景下不需要 Dark Mode,应该在做产品族定义时就明确"ChineseFamily 不支持 Dark Mode",而不是在工厂里加一个 if (theme == DARK && language == CHINESE) 的兜底分支。
抽象工厂和 Spring 容器的协作
在 Java 生态里,抽象工厂常被 Spring 的 DI 容器"抢活"。因为 Spring 本身就是一个超级工厂------它能在配置阶段就把所有 bean 装配好,业务代码 Autowired 一下就拿到对象。
但 Spring 解决不了"产品族一致性"问题。Spring 解决的是"单实例的依赖注入",而抽象工厂解决的是"在运行时根据上下文选择一组配套实例"。
实战中比较成熟的模式是:
java
@Component
public class UIFactoryProvider {
private final Map<String, UIFamily> families;
public UIFamily resolveFamily(String familyName) {
UIFamily family = families.get(familyName);
if (family == null) {
throw new IllegalArgumentException("未知产品族: " + familyName);
}
return family;
}
}
UIFamily 是抽象工厂的接口,每个实现(ChineseFamily、MaterialYouFamily)都是 Spring 容器里的一个 bean。UIFactoryProvider 负责根据运行时上下文(用户选的语言、客户端主题)找出对应的产品族。
这种"抽象工厂 + Spring DI"的组合,把"工厂模式"和"产品族"两个概念都发挥到极致------Spring 负责装配抽象工厂的实现,抽象工厂负责保证产品族内部的组件一致性。
抽象工厂的三个真实适用场景
讲了这么多否定面,来说说抽象工厂真正适合的场景。
场景一:跨数据库方言。 你做一个 ORM 框架,要支持 MySQL、PostgreSQL、Oracle。但方言不是只有"SQL 语法"------是"SQL 语法 + 数据类型 + 函数 + 索引类型 + 事务隔离级别"这一整套。每个方言是一族产品,必须一起出现。Hibernate 的 Dialect 类就是抽象工厂的一个经典实现。
场景二:跨主题 UI。 我们的麻辣烫话题------UI 主题的本质是"字体 + 配色 + 间距 + 交互细节"这一整套的组合。真正的"产品族"边界是"主题本身",而不是"语言 × 主题"。
场景三:跨平台 SDK 适配。 你的 SDK 要支持 iOS、Android、Web 三端。每个端是一个产品族。SDK 内部的"网络层 + 存储层 + UI 桥接层"必须来自同一个端------你不能用 iOS 的网络层配 Android 的 UI 桥接。
跨数据库方言、跨UI主题、跨平台SDK,都是"组合必须配套"的场景。这就是抽象工厂的用武之地。
抽象工厂的反模式
也有几种场景,不要用抽象工厂:
只有一种产品时。 工厂方法就够了,用抽象工厂是过度设计。
产品族之间没有强一致性约束。 比如你的"检索服务"和"日志服务",互相之间没有任何一致性关系,强行套抽象工厂只会增加无意义的间接层。
"产品族"是动态生成的。 比如用户上传自定义主题。这种场景下抽象工厂的"族"概念本身就不成立,应该用配置中心 + 模板引擎,而不是抽象工厂。
家族成员经常变化。 如果一个产品族里今天加一个组件、明天减一个组件,抽象工厂接口会频繁修改,违反开闭原则。这种情况更适合用 Builder 模式或者配置驱动。
一句话总结
抽象工厂的真正价值,是把"产品族一致性"这个约束,用类型系统+工厂接口的方式固化下来。它不是创建对象的工具,是防止你把不兼容的对象配在一起的守门员。
下次你看到项目里有一组对象必须"配套出现",先想清楚产品族边界是什么------这个边界画错了,抽象工厂再怎么花哨都没用。
我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章。