你以为抽象工厂是「创建一组对象」——其实它是「锁定产品族兼容性」

你以为抽象工厂是「创建一组对象」------其实它是「锁定产品族兼容性」

抽象工厂(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 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章。

相关推荐
阿弱1 小时前
graph-core 的边与命令模式设计
java·后端·agent
长栎1 小时前
你的 AI 品控规则越来越多了——但它们已经在打架了,你没看见
后端
有脚就行1 小时前
第24篇-Go-gRPC推理服务-高性能跨语言通信
开发语言·人工智能·后端·golang
不好听6131 小时前
NestJS 是什么?一张图看懂企业级后端的骨架
后端·nestjs
liuxiaocheng1 小时前
文本生成的进阶:generateText / streamText 里迟早会撞上的东西
前端·后端·ai编程
江小渔1 小时前
训练工程与训练平台入门33 追踪一次 TrainJob 的完整生命周期
后端
无糖可可果1 小时前
NestJS 学习分享:从工厂模式到企业级后端开发
后端
vipbic2 小时前
网站升级了,我却有点舍不得
前端·vue.js·后端
IT_陈寒2 小时前
Java Stream并行处理让我数据库崩了两次
前端·人工智能·后端