1. 先解决一个真问题:产品成族出现时,直连 new 会让你崩溃
1.1 一个多端 UI 需求引发的噩梦
很多朋友学设计模式,学到抽象工厂这一节就开始打瞌睡。UML 图看了三遍,每个角色名字都记住了,合上书还是不知道它到底解决什么问题。我早年在做一套报表系统的时候,也栽过同样的跟头——不是看不明白定义,而是被一个真实的需求逼到墙角之后,才发现那些抽象角色其实离日常编码非常近。
抽象工厂模式(Abstract Factory Pattern)是创建型设计模式里最容易被误解、也最容易被低估的一个,它解决的核心问题不是"怎么生成一个对象",而是"怎么保证一组对象永远配套出现"。如果你写过跨端 UI、适配多套主题、或者对接过多种数据库,这篇文章应该能帮你在 20 分钟内把它彻底用明白。
我先说一个很多前端和客户端开发都遇到过的场景。公司要做一套报表系统,界面需要做两套风格:Windows 上跑一套原生 Windows 观感,Mac 上跑一套 Mac 观感。抛开真正跨平台方案不谈,假设产品经理明确要求"不同平台用不同控件库",你最开始会怎么写?新手写法通常是两层 if 嵌套:
if (isWindows) { new WindowsButton().render(); } else { new MacButton().render(); }一个按钮还好,但报表系统怎么可能只有一个按钮?很快你会遇到工具条上的按钮、查询区的文本框、筛选项里的下拉框、弹窗、分页组件……每一个控件,都要判断一次当前平台。代码写到最后,整个页面渲染方法变成了一个几十行的 if/else 大杂烩。最要命的是:过两个月要新增一个 Linux 端,你必须打开每一个渲染页面,逐个补else if。漏改一次,线上就会混出来一套四不像界面。
这个场景不是我编的,是很多人在小型项目里真实踩过的。我甚至见过一个老系统,页面渲染逻辑里塞了七八个平台判断分支,后来接新平台时,运营在测试环境随便一点就发现弹窗还是旧风格,查了半天才发现某个工具类里的分支没补上。它背后藏着两个痛点:第一,对象的创建逻辑散落在各处,改一处漏一处;第二,一组对象之间必须保持"风格一致",但没有任何机制来保证这一点。
1.2 每个对象都用工厂方法,为什么还是不够
有人会说:"创建逻辑散落?那我用工厂方法模式把创建动作收拢起来不就行了?"
工厂方法确实能解决"单个对象怎么创建"的问题。你可以写一个ButtonFactory,根据平台参数返回不同按钮;再写一个TextBoxFactory,返回不同输入框。表面上你很优雅,代码也好读了,但一个隐藏问题马上冒出来:谁来保证这两个工厂的选择是一致的?
换句话说,ButtonFactory不知道TextBoxFactory选了哪个平台。如果 A 同事把按钮的创建逻辑写成了if (isWindows),B 同事把输入框的创建逻辑写成了if (!isMac),在 Windows 平台上,你很可能得到一个 Windows 按钮 + Mac 输入框的组合,而程序本身完全没有报错,直到测试截图发到群里你才发现界面已经乱了。
所以,当我们需要的不只是一个对象,而是一组"必须配套出现"的对象时,单纯拆出几个工厂方法是不够的。我们需要一个更高层的东西,一次决定"这一整套都用哪个风格"。
1.3 产品族与产品等级:理解抽象工厂的第一把钥匙
要彻底理解抽象工厂,必须先分清两个词:产品族(Product Family)和产品等级结构(Product Hierarchy)。
拿家具举例。椅子和沙发是两种不同的产品类型,椅子下面有现代椅、古典椅;沙发下面有现代沙发、古典沙发。这里:
- 产品等级结构:椅子、沙发这种"同一类产品"的继承/实现关系;
- 产品族:现代风格椅 + 现代风格沙发 + 现代风格茶几,这种"风格一致的一组产品"。
每个具体产品,其实是"产品类型"和"产品风格"这两条坐标轴交叉出来的格子。现代椅是"现代"与"椅子"的交叉,古典沙发是"古典"与"沙发"的交叉。
抽象工厂干的事,翻译成生活语言就是:你不需要自己分别去挑椅子和沙发,你只需要告诉商家你要"现代风格"还是"古典风格",商家会把整套风格统一的家居给你配齐。订单里绝不会出现现代椅子配古典沙发的操作,因为那是两个不同工厂干的事。
提示:判断一个场景适不适合用抽象工厂,就看"这一组对象如果混搭了,系统会不会出问题"。会出问题,就是抽象工厂的主场;不会出问题,用工厂方法就够。
2. 抽象工厂的结构其实就四张牌
2.1 四个角色的分工
抽象工厂模式整个结构里只有四个角色,把它想成一张打牌的桌面就够了:
- 抽象工厂(AbstractFactory):定义一组创建方法的接口,比如
createChair、createSofa。它只负责"声明我能创建哪些东西",不关心到底会创建出什么具体对象。 - 具体工厂(ConcreteFactory):不同风格的实现,比如
ModernFurnitureFactory、ClassicalFurnitureFactory。每个具体工厂负责把"同一个风格"的一整套具体产品组装好。 - 抽象产品(AbstractProduct):一族产品的共同接口,比如
Chair、Sofa。 - 具体产品(ConcreteProduct):某个具体工厂产出的具体对象,比如
ModernChair、ClassicalSofa。
客户端(调用者)只依赖抽象工厂和抽象产品,所有具体类都藏在具体工厂内部。这样,客户端写代码时面对的就只有"接口",没有"实例怎么来、从哪来"的负担。
2.2 结构骨架代码一眼看懂
先别急着写业务,我们用一组最简洁的骨架代码看看这套结构长什么样:
// 抽象产品 public interface Chair { void sit(); } public interface Sofa { void lie(); } // 抽象工厂:约定"一套家具应该包含哪些成员" public interface FurnitureFactory { Chair createChair(); Sofa createSofa(); } // 具体产品:现代椅 public class ModernChair implements Chair { public void sit() { System.out.println("坐在现代椅上"); } } // 具体产品:古典沙发 public class ClassicalSofa implements Sofa { public void lie() { System.out.println("躺在古典沙发上"); } } // 具体工厂:现代风格,负责生产一整套现代家具 public class ModernFurnitureFactory implements FurnitureFactory { public Chair createChair() { return new ModernChair(); } public Sofa createSofa() { return new ModernSofa(); } } // 具体工厂:古典风格,负责生产一整套古典家具 public class ClassicalFurnitureFactory implements FurnitureFactory { public Chair createChair() { return new ClassicalChair(); } public Sofa createSofa() { return new ClassicalSofa(); } }看仔细了:ModernFurnitureFactory的createChair永远返回ModernChair,createSofa永远返回ModernSofa。也就是说,只要客户端拿到的是ModernFurnitureFactory,它创建的整套产品就一定都是现代风格。风格的一致性在工厂内部就被约束住了,而不是靠调用方自觉。这个约束比任何代码规范都可靠,因为它是从类型层面强制出来的。
2.3 客户端为什么能"不知情"地拿到正确对象
这套结构能运转的底层逻辑,其实就是面向接口编程 + 运行时多态,两句话的事。
第一,客户端持有的工厂变量,类型永远是抽象工厂FurnitureFactory。至于真正传进来的对象是ModernFurnitureFactory还是ClassicalFurnitureFactory,客户端根本不在乎,也不应该在乎。
第二,客户端持有的产品变量,类型也永远是抽象产品Chair、Sofa。createChair()返回的实例在运行时是ModernChair,但客户端调用sit()时,根本不需要知道它的真实类型,多态会自动帮你调用到正确实现。
这两条加在一起,客户端代码就成了一个"只问接口、不问实现"的纯消费者。真正决定"用哪一套风格"的地方,往往只在程序入口处,或者在一个配置加载模块里。这种"工厂选择权上移"的做法,让后续扩展新风格变得格外轻松。
3. 手把手实现:跨平台 UI 组件库完整 Demo
3.1 需求与建模:两个抽象产品、两家具体工厂
理论说完了,下面用"跨平台 UI 组件库"这个经典例子完整写一遍。需求是这样的:应用需要根据运行平台(Windows / Mac)创建对应风格的按钮和输入框,而且要保证同一个平台下,按钮和输入框一定来自同一风格。我故意不选数据库那种偏底层的例子,因为 UI 场景最直观,谁都能看懂按钮和输入框搭配不当有多违和。
建模分三步:
- 抽出抽象产品:
Button(按钮)、TextBox(输入框); - 为每个平台各实现一套具体产品:
WindowsButton、WindowsTextBox、MacButton、MacTextBox; - 抽出抽象工厂
UIFactory,里面声明createButton()和createTextBox(),再为两个平台各写一个具体工厂。
这个过程里最容易踩的坑是:一上来就写接口实现,结果接口设计得过大,后面的具体类连继承关系都对不上。我的习惯是先列一张产品类型清单(按钮、输入框),再列一张平台清单(Windows、Mac),确认交叉格子是闭合的,再动手写类。
3.2 完整可运行代码
下面是完整的 Java 实现,我把所有类放在一个文件里展示,方便你直接跑:
// ---------- 抽象产品 ---------- public interface Button { void render(); } public interface TextBox { void show(); } // ---------- 具体产品:Windows 风格 ---------- public class WindowsButton implements Button { @Override public void render() { System.out.println("渲染一个 Windows 风格的按钮"); } } public class WindowsTextBox implements TextBox { @Override public void show() { System.out.println("显示一个 Windows 风格的输入框"); } } // ---------- 具体产品:Mac 风格 ---------- public class MacButton implements Button { @Override public void render() { System.out.println("渲染一个 Mac 风格的按钮"); } } public class MacTextBox implements TextBox { @Override public void show() { System.out.println("显示一个 Mac 风格的输入框"); } } // ---------- 抽象工厂 ---------- public interface UIFactory { Button createButton(); TextBox createTextBox(); } // ---------- 具体工厂:Windows 一族 ---------- public class WindowsFactory implements UIFactory { @Override public Button createButton() { return new WindowsButton(); } @Override public TextBox createTextBox() { return new WindowsTextBox(); } } // ---------- 具体工厂:Mac 一族 ---------- public class MacFactory implements UIFactory { @Override public Button createButton() { return new MacButton(); } @Override public TextBox createTextBox() { return new MacTextBox(); } } // ---------- 客户端:应用主体只认识抽象工厂 ---------- public class Application { private final UIFactory factory; private Button button; private TextBox textBox; public Application(UIFactory factory) { // 把工厂从外部注入,而不是在这里自己选 this.factory = factory; } public void init() { button = factory.createButton(); textBox = factory.createTextBox(); } public void paint() { button.render(); textBox.show(); } public static void main(String[] args) { String osName = System.getProperty("os.name").toLowerCase(); UIFactory factory; // 工厂选择集中在入口处,方便日后改成配置驱动 if (osName.contains("win")) { factory = new WindowsFactory(); } else { factory = new MacFactory(); } Application app = new Application(factory); app.init(); app.paint(); } }运行起来很简单:你在 Windows 上跑,输出两行;在 Mac 上跑,输出另外两行。因为System.getProperty("os.name")拿到的是实际平台字符串,所以这套代码天然跟随运行环境切换组件风格。
3.3 运行结果与调用链分析
这段程序的调用链是理解抽象工厂的关键,我拆开说:
main方法根据系统属性拿到osName,在入口处决定用哪一个具体工厂;Application构造函数把工厂注入并保存,应用内部此后不再出现任何WindowsFactory/MacFactory字样;init()里调用factory.createButton()和factory.createTextBox(),实际返回的是对应平台的WindowsButton+WindowsTextBox,或者MacButton+MacTextBox;paint()里只调用抽象产品的公共方法render()/show(),由多态把调用分发到具体实现。
注意第三步:如果main选了WindowsFactory,那init()里拿到的按钮和输入框就必然都是 Windows 风格。你想让它们变成一混一?做不到,因为这个工厂在内部就把配对写死了。这正是抽象工厂相对于工厂方法的不可替代之处——它不是靠约定,也不是靠代码审查,而是从对象创建的那一刻起就锁死了整族的属性。
4. 实战验证:新增一个 Linux 风格要改几行代码
4.1 按步骤添加 Linux 风格
现在需求方来说:"我们也要支持 Linux 桌面端。"如果之前的代码里全是散落的 if/else,这一步会让很多人头皮发麻;但在抽象工厂的结构下,步骤非常固定。
第一步,新增两个具体产品类:
public class LinuxButton implements Button { @Override public void render() { System.out.println("渲染一个 Linux 风格的按钮"); } } public class LinuxTextBox implements TextBox { @Override public void show() { System.out.println("显示一个 Linux 风格的输入框"); } }第二步,新增一个具体工厂:
public class LinuxFactory implements UIFactory { @Override public Button createButton() { return new LinuxButton(); } @Override public TextBox createTextBox() { return new LinuxTextBox(); } }第三步,只在入口的选择逻辑里加一个分支:
if (osName.contains("win")) { factory = new WindowsFactory(); } else if (osName.contains("linux")) { factory = new LinuxFactory(); } else { factory = new MacFactory(); }完成。整个过程中,我没有动过Application、Button、TextBox、WindowsButton、MacButton任何一个既有的类。新来的同事看到这段代码,也不需要理解整个渲染链路,只要照着 WindowsFactory 的样子抄一个 LinuxFactory 出来,再把新工厂挂到入口处就行。这种"照着既有模式抄作业"的体验,在真实团队协作里特别重要,因为大部分维护代码的人根本不是你。
4.2 改动面到底有多大:符合开闭原则
我们把改动清单列出来对比一下:
| 类型的类 | 本次增加 Linux 时是否需要改动 |
|---|---|
抽象产品接口Button/TextBox | 不需要 |
抽象工厂接口UIFactory | 不需要 |
客户端Application | 不需要 |
| 既有具体产品(Windows / Mac 系列) | 不需要 |
| 既有具体工厂(Windows / Mac 工厂) | 不需要 |
| 新增 Linux 系列产品类 | 新增 2 个类 |
| 新增 Linux 工厂类 | 新增 1 个类 |
| 入口处平台判断 | 增加 1 个分支 |
这就是设计模式里经常说的"对扩展开放,对修改关闭"(开闭原则)的直观体现。新增一个产品族(新风格),只需要新增类,不需要修改已有类。
不过我在这里要泼一盆冷水:现实项目里,入口处的那个分支通常也做不到百分百不动。但你把它收敛成了一个很小的决策点,而不是散落在几十个页面里,这已经是质变了。真正的可维护性,从来不是"一行代码都不改",而是"改动都集中在一个可预期、可控制的地方"。
4.3 反过来看:新增一种"产品种类"时为什么疼
抽象工厂最大的软肋,不是新增风格,而是新增产品类型。
假设产品经理拍板:"所有平台都要加一个下拉框 Dropdown。"你会遇到什么?你需要在抽象工厂接口里加一个createDropdown()方法,然后在WindowsFactory、MacFactory、LinuxFactory这三个具体工厂里全部实现一遍,再写WindowsDropdown、MacDropdown、LinuxDropdown三个新类。
这意味着:每次扩展产品等级结构,既有所有工厂都要跟着改。这就像卖家具的店铺,每次新进一种"座椅"品类,所有风格的门店仓库都要同步上架。如果业务演进速度很快,产品类型隔三差五就加一种,抽象工厂反而会成为修改风暴的中心。
提示:抽象工厂的优缺点是对称的——一族内配套好,跨族难混搭;一族好扩展,一类难新增。选择前请务必对两个方向的演变频率做一次预判。这不是选型时的优点,而是选型前就必须接受的代价。
5. 抽象工厂和工厂方法,别再傻傻分不清
5.1 一张表讲清核心差异
好多人在面试时会把"抽象工厂"和"工厂方法"说混,其实两者的发力点完全不同。我用一张表把它们钉死:
| 对比维度 | 工厂方法模式 | 抽象工厂模式 |
|---|---|---|
| 关注点 | 单个产品对象的创建 | 一组配套产品的创建 |
| 产品数量 | 一个产品等级结构 | 多个产品等级结构 |
| 配套保证 | 不保证,各管各的 | 强制保证一族产品风格一致 |
| 核心动作 | 子类重写工厂方法来决定具体产品 | 具体工厂实现多个创建方法,整体返回一套产品 |
| 扩展方向 | 新增具体产品类时,只需新增对应子类 | 新增产品族容易,新增产品类型较难 |
| 典型结构 | 一个抽象类 + 多个子类工厂方法 | 一个抽象工厂接口 + 多个具体工厂类 |
更直白一点:工厂方法解决的是"同一个产品,由哪个子类去造";抽象工厂解决的是"一整套不同产品,如何保证来自同一个家族"。一个是点,一个是面。
5.2 按"产品数量"和"产品族"两个维度做选型
我把实际选型时的判断逻辑总结成一句话:先数产品等级,再看要不要配套。
- 如果系统里只有一种产品等级(比如只有按钮),不同平台只是按钮的不同实现,那用工厂方法就够了,不需要上抽象工厂;
- 如果系统里有多种产品等级(按钮、输入框、下拉框),而且这些产品强相关、必须保持一致,那抽象工厂是天然选择;
- 如果这些产品等级之间没有配套要求,也就是可以自由混搭,其实你用几个独立的工厂方法或简单工厂都行,抽象工厂反而会制造不必要的约束;
- 如果创建过程很复杂,比如要配置颜色、尺寸、事件监听,那抽象工厂里每个创建方法内部可以再结合 Builder 模式,把复杂对象的组装细节继续封装下去。
我在实践中还有一个更"懒"的判断法:一家工厂接口里至少要出现两个以上的创建方法,才值得叫抽象工厂;如果只有一个创建方法,它就是披着抽象工厂外衣的工厂方法。这个判断标准能帮你快速过滤掉很多为了模式而模式的类,也经常在代码评审阶段帮我拦住那些"过度抽象"的同事。
6. 现实世界里的抽象工厂:数据库层、UI 主题、框架内部
6.1 Swing 的 LookAndFeel:教科书级别的现身说法
Java 的老牌 GUI 框架 Swing,内部就藏着一套抽象工厂的经典实例,叫 LookAndFeel(外观)。你可以通过一行代码切换整个界面的风格:
UIManager.setLookAndFeel(new WindowsLookAndFeel());这行代码一执行,界面上的按钮、菜单、滚动条、文件选择器全部变成 Windows 风格。你再看它的实现:MetalLookAndFeel、WindowsLookAndFeel、MotifLookAndFeel 分别对应不同的具体工厂,每个工厂都会成体系地返回自己那套组件外观实现。如果你能理解 LookAndFeel 的机制,其实就相当于在一个真实的大型框架里看到了抽象工厂模式。
6.2 数据库方言 Dialect:产品族思想在 ORM 中的应用
另一个非常典型、但很多人没意识到的地方,是 ORM 框架里的数据库方言(Dialect)。
以 Hibernate 为例,MySQL、Oracle、PostgreSQL 对分页 SQL、序列生成、自增主键的语法各不相同。Hibernate 为每种数据库准备了一个Dialect子类,例如MySQLDialect、OracleDialect。当你切换hibernate.dialect配置时,框架内部会同时切换掉一整组 SQL 生成策略:分页用的LIMIT写法、查询序列的方式、判断空值的函数……全部都来自同一个方言家族。
你可以想象一下,如果分页用一种数据库的写法,序列查询用另一种数据库的写法,应用上线后 Oracle 上跑 MySQL 的 SQL,那画面得多美。"必须整套配套使用"这个约束,放在数据库方言场景里几乎是生死线,这正是抽象工厂适合的土壤。
6.3 商业项目里抽象工厂为什么常被"简化"
不过,看多了业务代码你会发现,真正在一线业务里手写AbstractFactory的场合并没有书里那么多。多数时候,大家会用更轻量的方式达到同样的效果。
比如 Spring 项目里,你完全可以用@Configuration加@Bean把不同策略的实现注册成 Bean,再根据配置项注入;或者用一个Map<String, SomeFactory>做策略注册表。效果上它同样实现了"把一组相关对象的创建集中起来、按需切换",只是省去了那些抽象接口的繁琐声明。
这不代表抽象工厂没有用,而是说明:设计模式的思想可以迁移,载体可以变化。你真正需要带走的核心能力,是识别"哪些对象必须成套出现"的建模嗅觉,而不是背住某个模式的类图。看框架源码时能认出它的影子,自己写业务代码时懂得用更顺手的等价物替代,这比死记硬背模式名有用得多。
7. 我踩过的坑:抽象工厂最容易翻车的三个地方
7.1 为了模式而模式:只有一个产品线也硬上抽象工厂
早年我见过一份代码,抽象工厂接口里声明了createUserService()、createOrderService()、createReportService()三个方法,看起来非常"规范"。但如果你仔细想,这三个业务服务之间根本不存在"风格必须一致"的关系,它们只是恰好都需要被创建而已。
这是典型的为了模式而模式。抽象工厂的价值在于约束产品族之间的配套关系;如果没有这种配套约束,硬把不相关的对象塞进同一个工厂接口,只会让工厂随着业务膨胀成一个新的"上帝类",比不用设计模式还难受。
我现在的做法是:先想清楚这些对象是不是真的互相绑定、必须成套;如果不是,就不要让它们住在同一家工厂里。有时候把创建逻辑拆成几个独立的小工厂,或者干脆用依赖注入容器去管理,反而是更干净的选择。
7.2 产品族扩展难,被产品种类牵着走
这部分我在 4.3 里已经详细讲过:产品族方向(加新风格)好扩展,产品等级方向(加新类型)难扩展。这个不对称性,很多人一开始根本没想到。
解决方案也不是没有。如果你的业务注定要在产品等级方向频繁扩展,可以考虑把抽象工厂退化为"注册表 + 泛型创建"形式,让具体工厂通过一个create(Class<T> type)方法来创建任意类型,避免每加一个产品类型就改动所有工厂:
public interface UIFactory { <T> T create(Class<T> componentType); }然后每个工厂内部维护一个从Class到具体构造函数的路由表。这种写法牺牲了一点类型安全,换来了产品等级方向上的良好扩展性。用不用,取决于产品类型变更的频率。如果三五年才加一个新控件,老老实实用标准抽象工厂反而更稳。
7.3 工厂越抽象越复杂,不如用 Map 注册表 + 反射
这是我在好几个真实项目里踩完之后总结的:如果具体工厂的数量不多,但入口选择逻辑总是变来变去,那就别把工厂选择写死在 if/else 里,直接用一个注册表来解决。
public class UIFactoryRegistry { private static final Map<String, UIFactory> FACTORIES = new HashMap<>(); static { FACTORIES.put("windows", new WindowsFactory()); FACTORIES.put("mac", new MacFactory()); FACTORIES.put("linux", new LinuxFactory()); } public static UIFactory get(String platform) { UIFactory factory = FACTORIES.get(platform.toLowerCase()); if (factory == null) { throw new IllegalArgumentException("未知平台: " + platform); } return factory; } }调用方只需要UIFactoryRegistry.get("linux")一行,就能拿到对应的整套工厂。以后加新平台,只需在注册表里多放一行,不需要动任何业务逻辑。
注意:注册表方式把创建逻辑集中到了一个 Map 里,牺牲掉的是一部分编译期类型检查——你不能再指望编译器提醒你某个工厂没实现某个新方法,必须靠单元测试去兜底。这个取舍要权衡好。
如果连初始化也想省,我还会配合反射扫描注解,让系统启动时自动发现所有带有@UIFamily("windows")标记的工厂类并注册进去。这算是把抽象工厂和现代框架的组件扫描思路结合了一波,项目里效果很好。
8. 最后分享两个实践技巧
8.1 技巧一:让 IoC 容器来当"工厂的妈妈"
在 Spring 这类容器框架里,抽象工厂往往不需要你手动维护生命周期。你可以为每种产品族写一个独立的配置类,容器一启动就把它变成 Bean;调用方只依赖抽象工厂接口,具体的工厂 Bean 由容器按条件自动注入:
@Configuration public class UIFactoryConfig { @Bean @ConditionalOnProperty(name = "ui.platform", havingValue = "windows") public UIFactory windowsFactory() { return new WindowsFactory(); } @Bean @ConditionalOnProperty(name = "ui.platform", havingValue = "mac") public UIFactory macFactory() { return new MacFactory(); } }这样"选择哪个工厂"这件事就彻底从代码里挪到了配置里。换一套 UI 风格只需改配置文件,连重新编译都不需要。这应该是现代项目里最舒服的落地方式,编译期类型安全还在,运行时切换还方便。
8.2 技巧二:先出现两个产品族,再上抽象工厂
最后说点个人经验。如果你的项目目前只有一套风格,不管界面还是业务,我建议先别急着抽象。我过去有一个习惯性动作:提前把未来才可能出现的第二个产品族预算出来,结果写出来的工厂接口又大又空,等到真正需要第二个风格时,发现当初的抽象方向跟实际需求对不上,白白重构了一次。
现在我给自己立了一条规矩:至少在代码里已经明确出现两个产品族的需求时,才动手引入抽象工厂。第一个产品族直接用最简单的方式实现,第二个需求出现时再重构,代价通常完全可控。这个"两次以上再抽象"的节奏,能帮你规避掉 80% 的过度设计。
抽象工厂模式讲到底,不复杂:它就是在告诉你,当一批对象必须成团出现时,给它们一个统一的团长。剩下的,都是组织代码的手艺罢了。