1. 从一个真实需求说起:为什么需要抽象工厂
我最早接触抽象工厂模式,是在做一个跨平台桌面应用的时候。当时产品经理给的需求很直接:“同一个管理后台,在 Windows 上跑要像 Windows 原生套件,在 macOS 上跑要像 Mac 原生套件,按钮、输入框、弹窗、复选框都得跟着平台走。”
听起来好像就是换一套皮肤,但在代码层面,事情没那么简单。你会发现:Windows 风格下,按钮是直角、深色调、点击时带阴影;复选框的选中态是“对勾加蓝色高亮”,弹窗的边距、圆角、按钮排列顺序都和 macOS 版本完全不同。如果直接把按钮、输入框、复选框这些组件各写一套 if-else,上来先判断当前系统是哪个平台,然后 new 不同的对象——代码会迅速膨胀,而且每加一种平台、每加一种组件,所有判断点都要跟着改一遍。
抽象工厂模式(Abstract Factory Pattern)解决的就是这类“一族相关对象”的创建问题。 它把一组有关联的产品对象(按钮、输入框、复选框)当成一个“产品族”,用同一个工厂接口来约束整个族内对象的创建。客户端不关心具体实例化了哪个类,只关心“我拿到的是一整套能配套使用的东西”。
这个模式适合谁看呢?适合所有写过业务系统、遇到过“同一个功能要适配多种环境/多种风格/多种厂商”的开发人员。哪怕你现在用不上,理解了它,你在设计系统的数据访问层、第三方对接层、主题切换模块时,都会比别人多想一层“产品族一致性”的问题。这篇文章我尽量用大白话拆开讲,穿插完整的 Java 代码示例、对比分析和我在实际项目中踩过的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 抽象工厂模式的结构与原理解读
2.1 核心角色与类图逻辑
抽象工厂模式一共有五个核心角色,理解这五个角色,基本就理解了这个模式的全部:
- 抽象产品(Abstract Product):为一类产品定义接口。比如“按钮”,不管 Windows 按钮还是 Mac 按钮,都是按钮,都要响应点击、都要渲染。
- 具体产品(Concrete Product):实现抽象产品接口的具体类。WindowsButton、MacButton 就是具体产品。
- 抽象工厂(Abstract Factory):声明一组创建产品的方法,每个方法对应一个抽象产品。比如 createButton()、createCheckbox()。
- 具体工厂(Concrete Factory):实现抽象工厂中的方法,生产出某一整套具体产品。WindowsThemeFactory 生产 WindowsButton、WindowsCheckbox,MacThemeFactory 生产 MacButton、MacCheckbox。
- 客户端(Client):只依赖抽象工厂和抽象产品接口,不接触任何具体类。
这里有一个非常关键的约束:抽象工厂里的方法是一组、一个族的,不是孤立的单方法。 如果接口里只有一个 createButton(),那它就退化成简单工厂或工厂方法模式了。抽象工厂强调的是一组方法的“捆绑关系”——保证“Windows 工厂只能产出 Windows 产品”“Mac 工厂只能产出 Mac 产品”,不会出现你买了 Windows 主题,弹出来的对话框却是 Mac 风格的怪异情况。
需要注意的是,产品族和产品等级结构是两个维度。按钮是产品等级结构(抽象按钮 → Windows 按钮 / Mac 按钮),窗口风格是产品族(Windows 全家桶 / Mac 全家桶)。抽象工厂管理的是产品族维度。
2.2 代码实现:跨平台 UI 组件系统
直接上一个能跑的最小示例,用 Java 写,语言不熟的同学看思想就好,逻辑比语法重要。
首先定义抽象产品,按钮和复选框:
java复制// 抽象产品:按钮
public interface Button {
void render();
void onClick();
}
// 抽象产品:复选框
public interface Checkbox {
void render();
void toggle();
}
然后是具体产品,Windows 风格一套,Mac 风格一套:
java复制// 具体产品:Windows 按钮
public class WindowsButton implements Button {
@Override
public void render() {
System.out.println("渲染 Windows 风格按钮:直角、深色、带阴影");
}
@Override
public void onClick() {
System.out.println("Windows 按钮点击效果:闪烁边框");
}
}
// 具体产品:Mac 按钮
public class MacButton implements Button {
@Override
public void render() {
System.out.println("渲染 Mac 风格按钮:圆角、浅色、平滑过渡");
}
@Override
public void onClick() {
System.out.println("Mac 按钮点击效果:呼吸动画");
}
}
// 具体产品:Windows 复选框
public class WindowsCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("渲染 Windows 风格复选框:方块、蓝色对勾");
}
@Override
public void toggle() {
System.out.println("Windows 复选框切换:对勾加粗并显示选中背景");
}
}
// 具体产品:Mac 复选框
public class MacCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("渲染 Mac 风格复选框:圆形、绿色填充");
}
@Override
public void toggle() {
System.out.println("Mac 复选框切换:平滑填充动画");
}
}
接下来定义抽象工厂和具体工厂:
java复制// 抽象工厂:负责创建一整族 UI 组件
public interface UIThemeFactory {
Button createButton();
Checkbox createCheckbox();
}
// 具体工厂:Windows 主题工厂
public class WindowsThemeFactory implements UIThemeFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
// 具体工厂:Mac 主题工厂
public class MacThemeFactory implements UIThemeFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public Checkbox createCheckbox() {
return new MacCheckbox();
}
}
客户端代码只认抽象工厂和抽象产品:
java复制public class Application {
private Button button;
private Checkbox checkbox;
public Application(UIThemeFactory factory) {
// 客户端不关心具体类,只依赖接口
button = factory.createButton();
checkbox = factory.createCheckbox();
}
public void renderUI() {
button.render();
checkbox.render();
}
public static void main(String[] args) {
// 模拟运行时选择主题,通常来自配置文件或启动参数
String theme = "mac";
UIThemeFactory factory;
if ("windows".equalsIgnoreCase(theme)) {
factory = new WindowsThemeFactory();
} else {
factory = new MacThemeFactory();
}
Application app = new Application(factory);
app.renderUI();
}
}
跑一下 main 方法,输出如下:
code复制渲染 Mac 风格按钮:圆角、浅色、平滑过渡
渲染 Mac 风格复选框:圆形、绿色填充
你会发现,客户端 Application 从头到尾没有写过一句 new MacButton() 或 new WindowsCheckbox()。它只是接过一个 UIThemeFactory,然后调用 createXxx() 方法。这就是依赖倒置原则的最佳体现:客户端依赖的是抽象接口,具体对象的生产完全交给了工厂。
2.3 关键设计决策:为什么“一致性”比“便利性”更重要
很多人会问,我不用抽象工厂,直接写一个简单的 if-else 工具类,比如 UIComponentFactory.createButton(theme)、UIComponentFactory.createCheckbox(theme),效果看起来差不多,为什么非要抽象工厂?
答案就是:一致性保证。
简单工厂/工具类方式的问题是,产品之间没有绑定关系。工具类里有 createButton 和 createCheckbox 两个方法,它们内部可以各自独立判断 theme 参数。这会导致两种隐患:
- 如果传入的 theme 参数非法,createButton 可能默认返回 WindowsButton,而 createCheckbox 默认返回 MacCheckbox,形成混搭,运行期很难排查。
- 当新增产品类型(比如增加一个 Slider 滑块组件)时,必须在工具类中再写一个 createSlider 方法,并且要把所有分支再复制一遍。产品族越多,这种复制越多,最终变成一堆难以维护的重复判断。
抽象工厂从类型层面上就把“族”这个约束固定了。WindowsThemeFactory 永远只能产出 Windows 系列的组件,编译器就帮你拦截了“混搭”的可能。哪怕你传错工厂,也只会在整套风格上错,不会在单个产品上错。
还有一个容易被忽视的点:抽象工厂模式天然支持运行时切换产品族。 只要调用处传进来的具体工厂不同,整个应用外观就变了。很多现代应用的“主题系统”就是基于这个思路。我后来做过的 SASS 平台用户自定义主题功能,就是定义了 LightThemeFactory 和 DarkThemeFactory,分别在构造函数里把工厂注入各个页面模块,切换主题时只需要重新注入工厂,所有页面组件自动更新。这种方法比在几百个组件里各自维护一个主题判断变量要干净得多。
3. 抽象工厂与工厂方法的对比:什么时候该用哪个
3.1 一张表看清区别:创建粒度与扩展方向
工厂方法模式和抽象工厂模式经常被放一起比较,两者名字像、结构也像,但实际定位差别很大。我整理了一张对比表:
| 对比维度 | 工厂方法模式 | 抽象工厂模式 |
|---|---|---|
| 创建对象数 | 通常一次创建一个产品对象 | 一次创建一整套有关联的产品对象 |
| 核心意图 | 将“创建对象”延迟到子类实现 | 将“创建一套相关对象”整体封装 |
| 扩展维度 | 纵向扩展:新增产品类 | 横向扩展:新增产品族(或新增产品等级) |
| 客户端感知 | 客户端知道具体的工厂子类 | 客户端只知道抽象工厂,不感知具体实现 |
| 典型场景 | 一个日志记录器,按环境输出格式不同 | 一套跨平台 UI,按钮、输入框、弹窗全部配套 |
| 扩展新产品 | 比较友好,增加一个具体创建方法即可 | 困难,抽象工厂接口变更会影响所有具体工厂 |
一句话总结我的理解:工厂方法解决的是“一种产品在不同情况下的不同实现”问题,抽象工厂解决的是“一整套产品必须保持统一风格/统一约束”问题。
需要注意“扩展方向”这行。抽象工厂模式对“增加新的产品族”友好,比如从 Windows+Mac 两族扩展到支持 Linux 一族,只需要新增一个 LinuxThemeFactory,并实现所有 createXxx 方法,现有代码完全不用动。但如果你想在现有产品族里“增加一种新型产品”,比如 UI 组件中再加一个下拉框 Selector,就得去抽象工厂接口里加 createSelector(),然后所有具体工厂全部要改。这就是经典的“开闭原则的倾斜性”——开放扩展产品族,关闭修改内部结构。
3.2 选型建议与反模式信号
就我观察,很多初学者会在不该用抽象工厂的地方硬上抽象工厂,导致代码膨胀。这里给出几条选型经验:
- 如果产品之间没有强关联关系,比如 A 产品和其他产品完全独立、混搭使用没有副作用,那么用简单工厂或者工厂方法就够了。不是所有创建逻辑都需要套抽象工厂。
- 如果产品族的数量在可预见时间内不会增加,而且产品族之间差异不大,抽象工厂带来的抽象层级可能超出收益。过度设计比不够设计更常见。
- 如果新增产品类型的频率高于新增产品族的频率,建议优先考虑工厂方法,而不是抽象工厂。因为抽象工厂在新增产品等级结构时要修改抽象接口,这是比较重的操作。
3.3 反模式信号:什么时候你根本不需要它
除了选型,我也想提醒大家识别“反模式”信号。比如你的代码里只有一个抽象工厂接口,但只有两三个具体工厂,而且它们的 createXxx 方法里逻辑几乎一样,就是 return new 的对象不同——很多情况下这么做就是多余抽象。抽象工厂的主要价值在于“族内约束”,如果本身就没有约束需求,纯为了套模式而套,那维护成本远大于收益。
另外一个反模式信号是:把抽象工厂接口设计得和具体产品完全一一对应,接口里方法数量巨大,五六种产品全塞在一个工厂里。正常业务下,一个界面主题的组件数量在 5~8 个左右是合理的,但如果一个“工厂”要管二十多种产品,建议拆分成多个抽象工厂,按领域划分,否则任何产品等级结构的变动都会引发所有工厂的连锁修改。
4. 实战案例:把抽象工厂用到数据库访问层
4.1 场景设定:支持多数据库的企业应用
UI 主题的例子好理解,但实际工作中,我发现抽象工厂模式在数据访问层的应用更常见也更有说服力。假设你在做一个企业级管理系统,客户有的用 MySQL,有的用 PostgreSQL,未来还可能接到 Oracle 的订单。业务逻辑要操作数据库,但操作方式有好几个步骤:建立连接、创建 SQL 命令、执行查询、处理事务。不同数据库的 API 差异虽然被 JDBC 屏蔽了一部分,但连接串格式、分页语法、事务隔离级别、驱动参数都不同。
如果将数据库相关的操作分散在各个业务代码里,一旦要切换数据库,所有代码都要跟着改。这时抽象工厂就可以派上用场:把“连接”“命令”“事务管理”这些相互关联的对象作为一个产品族,MySQL 工厂产出 MySQL 版本,PostgreSQL 工厂产出 PostgreSQL 版本,业务层只依赖抽象工厂。
4.2 完整实现与参数设计
直接看代码。先定义抽象产品:
java复制// 抽象产品:数据库连接
public interface DBConnection {
void connect();
void disconnect();
}
// 抽象产品:指令
public interface DBCommand {
void execute(String sql);
ResultSet query(String sql);
}
// 抽象产品:事务管理器
public interface DBTransaction {
void begin();
void commit();
void rollback();
}
然后定义 MySQL 和 PostgreSQL 两个产品族,代码内容按 JDBC 写法和数据库方言差异给出:
java复制// MySQL 连接
public class MySqlConnection implements DBConnection {
private String url;
private String username;
private String password;
public MySqlConnection(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
@Override
public void connect() {
// 解析 jdbc:mysql:// 前缀,配置驱动参数
System.out.println("已连接 MySQL,URL = " + url);
}
@Override
public void disconnect() {
System.out.println("已断开 MySQL 连接");
}
}
// PostgreSQL 连接
public class PostgreSqlConnection implements DBConnection {
private String url;
private String username;
private String password;
public PostgreSqlConnection(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
@Override
public void connect() {
// 解析 jdbc:postgresql:// 前缀,加载 PostgreSQL 驱动
System.out.println("已连接 PostgreSQL,URL = " + url);
}
@Override
public void disconnect() {
System.out.println("已断开 PostgreSQL 连接");
}
}
命令和事务类同理,这里不全部展开。重点看抽象工厂和具体工厂:
java复制// 抽象工厂:数据库访问族工厂
public interface DatabaseFactory {
DBConnection createConnection();
DBCommand createCommand();
DBTransaction createTransaction();
}
// MySQL 具体工厂
public class MySqlDatabaseFactory implements DatabaseFactory {
private String url;
private String username;
private String password;
public MySqlDatabaseFactory(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
@Override
public DBConnection createConnection() {
return new MySqlConnection(url, username, password);
}
@Override
public DBCommand createCommand() {
return new MySqlCommand();
}
@Override
public DBTransaction createTransaction() {
return new MySqlTransaction();
}
}
// PostgreSQL 具体工厂
public class PostgreSqlDatabaseFactory implements DatabaseFactory {
private String url;
private String username;
private String password;
public PostgreSqlDatabaseFactory(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
@Override
public DBConnection createConnection() {
return new PostgreSqlConnection(url, username, password);
}
@Override
public DBCommand createCommand() {
return new PostgreSqlCommand();
}
@Override
public DBTransaction createTransaction() {
return new PostgreSqlTransaction();
}
}
业务层只需要接收一个 DatabaseFactory 接口,就可以完成整套数据库操作:
java复制public class UserRepository {
private DatabaseFactory dbFactory;
public UserRepository(DatabaseFactory dbFactory) {
this.dbFactory = dbFactory;
}
public void findUserById(int id) {
DBConnection connection = dbFactory.createConnection();
connection.connect();
DBCommand command = dbFactory.createCommand();
command.query("SELECT * FROM user WHERE id = " + id);
// 处理结果...
connection.disconnect();
}
}
以后来了 Oracle 需求,只需要写一个 OracleDatabaseFactory,实现 Oracle 的连接、命令、事务,业务层一行不改。
4.3 在实际业务中加入抽象工厂的步骤
如果要在现有业务里引入抽象工厂,我建议按以下步骤走,避免一次性改动过大:
- 圈定产品族边界。明确哪些对象必须配套使用、不能混搭。比如数据库连接、命令、事务必须配套,这是产品族。而日志工具、缓存工具和数据库操作没有强关联,就不要硬塞进族里。
- 提取抽象接口。把所有具体产品类的公共方法提取成接口。注意接口不要设计得太肥,只保留业务层真正使用的方法。
- 创建抽象工厂接口和具体工厂。这一步建议先写一个具体工厂验证可行性,再补全其他工厂,不要一上来就铺开。
- 修改业务层的依赖注入。把业务代码中所有
new XxxDao()的地方改成接收抽象工厂,由工厂负责创建部门或配置中心来装配具体工厂。 - 编写测试覆盖。重点测试产品族一致性——确保从同一个工厂创建出来的对象能协同工作,以及切换工厂后业务行为正常。
4.4 配置驱动的工厂装配:让系统真正“即插即用”
工厂模式的价值不止在于隔离创建逻辑,更在于配合配置中心实现“运行时切换产品族”。我处理数据库切换时,通常把工厂的选择逻辑抽成一个独立的装配类,工厂的类型可以通过配置文件或环境变量指定。
java复制public class DatabaseFactoryProvider {
public static DatabaseFactory getFactory() {
String dbType = ConfigManager.get("db.type", "mysql");
String url = ConfigManager.get("db.url");
String username = ConfigManager.get("db.username");
String password = ConfigManager.get("db.password");
switch (dbType.toLowerCase()) {
case "postgresql":
return new PostgreSqlDatabaseFactory(url, username, password);
case "mysql":
default:
return new MySqlDatabaseFactory(url, username, password);
}
}
}
业务代码只需要一次调用 DatabaseFactoryProvider.getFactory(),后续所有数据库操作自动跟随配置切换。这里是抽象工厂的另一个优势:集中管理工厂装配逻辑,避免散落判断。如果你没有抽象工厂层,要切换数据库可能得翻遍整个项目去替换每个 new 语句,而有了工厂层,这个变化只影响 Provider 内部一个 switch 分支。
5. 优缺点、应用场景与行业内实践
5.1 优点与代价
抽象工厂模式在优秀设计模式中算复杂度偏高的一个,动手写之前必须先理解它的收益和成本。
先说优点:
- 产品族内部一致性有保障。这是抽象工厂最核心的价值。同一个工厂生产的产品天然配套,从源头上杜绝了混搭。
- 客户端与具体类解耦。业务代码里不需要出现任何具体产品类名,只依赖抽象接口,配合依赖注入后代码的可测试性、可替换性都很强。
- 符合开闭原则(产品族维度)。新增一个产品族只需要新添加一个具体工厂和一批具体产品,已有代码完全不需要修改。
- 切换产品族方便。只要更换传入的具体工厂,整套产品族一起切换,适合配置驱动、插件系统的场景。
再看代价:
- 类数量急剧增加。每个产品族、每个产品类型都要新建类,小型项目可能出现类爆炸现象。
- 新增产品等级结构困难。这是最大的软肋,往抽象工厂接口里加一个方法,所有具体工厂都要同步修改,成本与影响面都不小。
- 抽象理解成本高。团队成员如果对设计模式不熟悉,初次接触容易混淆抽象工厂和工厂方法。
5.2 典型应用场景盘点
抽象工厂模式适合出现“产品族”和“系列产品”概念的场景,我列举几类比较典型的:
- 跨平台 UI 组件库:不同操作系统的按钮、文本框、菜单等组件构成一个产品族。你用的工具包甚至 IDE 中都有类似实现。
- 多数据库数据访问层:MySQL、PostgreSQL、Oracle 的连接、指令、事务是关联产品,用抽象工厂封装后业务层无需变更。
- 主题/皮肤系统:明暗主题、企业定制主题等,每种主题包含导航栏、按钮、卡片、表格等一组视觉组件,切换主题等于切换工厂。
- 云服务厂商适配层:同时对接多家云厂商时,对象存储、消息队列、短信服务等一组 SDK 操作可抽象为产品族,每家厂商一个具体工厂。
- 报表导出系统:PDF 报表、Excel 报表、CSV 报表分别有各自的写入器、格式生成器、元数据处理器,用抽象工厂可以保证不同格式的整套处理组件互不混用。
5.3 常见框架中的抽象工厂影子
你在很多框架里已经见过抽象工厂,只是当时没意识到。比如 Java 的 DocumentBuilderFactory 和 TransformerFactory,它们就是典型的具体工厂类,而解析器和转换器就是抽象产品。GUI 框架 Swing 的 LookAndFeel 也很接近抽象工厂思想——每种外观主题提供一整套组件绘制工厂。还有 Spring 框架中大量使用了工厂机制,BeanFactory 虽然不是严格意义上的抽象工厂,但它对“创建对象”这层抽象的思想是一脉相承的。
我自己的经验是,理解了抽象工厂之后,再看这类框架的源码,很多地方都能对号入座:框架怎么把创建细节隐藏起来、怎么保证同一套组件协同工作、怎么方便扩展新实现,这些都离不开“产品族”这层设计思维。
5.4 行业内实践:配置中心与工厂注册表结合
在大型企业应用里,抽象工厂经常和“工厂注册表”结合使用。具体工厂会被注册到一个 Map 中,key 是产品族的标识符(比如 "mysql"、"postgresql"),value 是工厂对象或工厂类的引用。这样装配代码只需要查这个注册表即可,连 switch 都不用写:
java复制public class DatabaseFactoryRegistry {
private static final Map<String, DatabaseFactory> FACTORY_MAP = new HashMap<>();
public static void register(String type, DatabaseFactory factory) {
FACTORY_MAP.put(type.toLowerCase(), factory);
}
public static DatabaseFactory getFactory(String type) {
DatabaseFactory factory = FACTORY_MAP.get(type.toLowerCase());
if (factory == null) {
throw new IllegalArgumentException("未注册的数据库类型: " + type);
}
return factory;
}
}
启动时完成注册:
java复制DatabaseFactoryRegistry.register("mysql", new MySqlDatabaseFactory(url, user, pass));
DatabaseFactoryRegistry.register("postgresql", new PostgreSqlDatabaseFactory(url, user, pass));
这样系统运行时,切换数据库类型只需要调用 DatabaseFactoryRegistry.getFactory("mysql")。不同团队还可以通过 SPI 或插件机制往注册表里动态新增产品族,业务层完全不用感知。这种模式非常适合对扩展性要求高的中大型系统,也是我推荐的一种实践方式。
6. 常见问题与排查技巧实录
6.1 问题列表与对策
我在实际代码评审和排障中发现,使用抽象工厂模式经常出现以下问题:
产品族混搭,没有从根源上杜绝。 有些项目名义上用了抽象工厂,但业务代码中还是有直接 new WindowsButton() 的地方。这种“类抽象工厂”模式比完全没有更危险,因为新加入的开发者无法判断到底该用工厂还是该直接 new。对策是代码评审阶段就要严格约束业务层只允许依赖抽象接口,发现直接 new 具体产品的地方一律打回修改。
抽象工厂接口设计得太肥或太瘦。 太肥是指把太多不相关的产品类型塞进一个工厂,导致新增一个产品时所有工厂都要改动;太瘦是指抽象工厂里只放一个方法,退化成工厂方法模式,失去了产品族约束的意义。设计时应该问自己:这些产品之间是否必须保持配套?如果答案模糊,很可能不该用抽象工厂。
工厂类和具体产品揉在一起。 我见过有些代码把工厂方法写在实体类里,比如现在 Button 类上直接加一个静态方法 createWindowsButton(),这种设计让实体类和创建逻辑耦合得太紧,抽象工厂的独立性就丧失了。正确姿势是工厂类和产品类各自独立。
没有处理好产品族扩展问题。 前面说过,抽象工厂对新增产品族友好,但新增产品等级结构代价大。如果业务里新增产品类型的频率比较高,建议不要在一开始就把抽象工厂接口设计成“全量产品”,可以预留一部分扩展点,或者在抽象工厂中用泛型方法支持新增产品类型,但这些属于高级用法,篇幅有限这里不展开。
6.2 我的实操心得与排查思路
如果你已经写了抽象工厂但运行行为不对,比如切换了主题但按钮没变,我一般按下面思路排查:
- 先确认具体工厂的选择逻辑是否走对。在
getFactory()入口打日志,确认传入的配置值是否是你想要的产品族。 - 再确认注入的目标是否正确。有时候代码里多个位置都创建了工厂,有的位置用了新工厂,有的位置还在用第一次创建的旧工厂。
- 最后检查具体产品内部状态。是否某些产品类中有静态变量或全局配置残留,导致新建的产品对象“看起来没变”。这种问题不太容易想到,但一出现往往就是静态状态污染。
我自己踩过的最严重的一个坑,是把工厂选择逻辑里的 switch 分支写错了默认值。当时数据库从 MySQL 切换到 PostgreSQL,配置文件明明写的 db.type=postgresql,但因为 switch 的 default 分支是 mysql,导致有一部分模块仍然用 MySQL 连接,排查了整整一个上午。后来我在所有工厂装配代码里强制要求:配置值必须精确匹配已注册的产品族,匹配不到直接报错,不许默认降级到某个隐藏值。 这样哪怕配置写错了,也能第一时间暴露出来,而不是留个暗坑在后面。
还有一个心得是:写抽象工厂的时候,建议把所有创建方法的参数设计成小零件对象,而不是传一大串基础类型。比如说 createConnection() 的参数不要写五个 String 分别是 host、port、database、user、password,而是定义 ConnectionConfig 对象,让调用方传入 config。这样等新产品族加入了新的配置项,比如需要传 SSL 证书路径,抽象工厂接口不需要变,只是 Product 类内部从 config 里取新字段即可。这个做法帮我避过很多次接口变更的麻烦。
6.3 从“套模式”到“理解模式”的转变
很多初学者学设计模式是先背结构再套代码,这样学抽象工厂会觉得它繁琐、不直观,因为你没有一个真实的问题驱动。我建议大家反过来:先复盘自己最近写的项目里有没有“需要保证产品族一致”“需要方便多个产品族之间切换”的痛点,有痛点再去看模式怎么解。
如果你没有接触过多平台、多数据库这类场景,抽象工厂对你来说可能就是纯理论,不需要硬用。设计模式的正确用法是“在问题的重压下逐渐浮现”出来,而不是“先有模式再找问题”硬搬过去。
最后再分享一个实际经验
我在做主题系统时,差点把抽象工厂模式引入到用户自定义主题里,后来发现产品族数量太多、组合空间巨大,抽象工厂根本兜不住。最终换成了组合模式加策略模式才解决问题。设计模式不是越多越好,同一个场景可能要结合多种模式才能解决真实世界的复杂问题,这点和写代码的很多事一样,权衡比套用更重要。希望这篇文章能帮你理解抽象工厂的适用边界,在真正需要产品族约束时想起它来。
