1. 为什么说抽象工厂模式是"工厂的工厂"
先提个问题:你写代码的时候,有没有遇到过这样的场景——一个业务模块里,因为要支持不同的数据库、不同的消息队列、不同的UI风格,导致if-else层层嵌套,每加一种新类型就要回头改一遍老代码?我早年做企业级系统的时候,被这种代码折磨得不轻。后来真正吃透了抽象工厂模式,才明白以前那些"看似能跑"的代码,问题不是出在功能上,而是出在扩展的代价上。
抽象工厂模式,英文是Abstract Factory Pattern,它是GoF二十三种设计模式里创建型模式的重要一员。它的核心思想可以一句话讲清楚:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。听起来有点绕,换个说法——它就是"工厂的工厂",先用一个工厂接口约定了"我要生产什么产品族",再用具体工厂决定"我实际生产哪个品牌的产品"。
这篇文章不是来给你念概念定义的,我想从实际的业务痛点出发,拆解抽象工厂模式到底解决了什么问题、它的结构长什么样、怎么落地、有哪些坑,以及它和普通工厂方法模式的本质区别。不管你是刚接触设计模式的新手,还是写了好几年业务代码想回头梳理一下,这篇文章都值得你花二十分钟读完。
先说个我自己的体会:设计模式这个东西,光看书上写的类图是没有感觉的,你只有把它放进一个你真正做过的项目里,才知道它好在哪里、坏在哪里。文末我会用一个跨平台UI控件库的完整案例,把抽象工厂模式从UML到代码到测试整个走一遍。你跟着敲一遍,绝对比死记硬背十个模式的类图有用。
2. 从真实业务困境出发:产品族一致性为什么难维护
2.1 混乱的调用方代码:我当初踩过的"硬编码"坑
假设你正在开发一套后台管理系统,运营说要支持两套风格:一套是面向内部员工的"极简蓝"主题,另一套是面向VIP客户的"尊贵金"主题。两套主题不仅颜色、字体不一样,连交互控件都不一样——比如蓝色主题下日期选择器是单面板,金色主题下日期选择器是双面板。
如果你的代码是这样写的:
public class LoginPage { private Button loginButton; private TextField usernameInput; private DatePicker datePicker; public LoginPage(String theme) { if ("blue".equals(theme)) { loginButton = new BlueButton(); usernameInput = new BlueTextField(); datePicker = new SinglePanelDatePicker(); } else if ("gold".equals(theme)) { loginButton = new GoldButton(); usernameInput = new GoldTextField(); datePicker = new DoublePanelDatePicker(); } } }看起来很直白对吧?问题出现在两个地方:
- 调用方(Client)被迫知道所有具体类的存在。你每写一个页面,都要在页面里维护一遍主题判断逻辑,这个"主题类型"的
if-else会散布在系统中几十上百个地方。 - 产品族的一致性完全没有保障。今天开发忘了在某个页面加双面板日期选择器,明天又有人在另一个页面把蓝色按钮和金色按钮混着用了。UI验收的时候,运营指着页面说"这个按钮怎么是蓝的?"——这种事故我经历过不止一次。
2.2 对象之间的"相互依赖"才是问题核心
抽象工厂模式解决的不是"创建单个对象"的问题,而是创建一套相互依赖的对象的问题。这句话值得你停下来多想三秒。
"相互依赖"是什么意思?比如在主题系统中,按钮和输入框之间可能存在对齐规则,日期选择器要跟文本框共用一套圆角风格。你不能把任意两个控件拼在一起就完事,它们必须作为一个整体出现。如果你用普通工厂方法模式(Factory Method),只能保证"一个工厂生产一种产品",但无法保证"多个工厂生产的多个产品之间能兼容"。
举个反例你就明白了。假如你有一个ButtonFactory,生产按钮;还有一个TextFieldFactory,生产输入框。这两个工厂各自独立,订单来了,你要的蓝色按钮和金色输入框都能造出来,但组合在一起丑得没法看。抽象工厂的意义,恰恰是把"蓝色主题下的所有控件"放在同一个工厂里,让它们天然配套。
所以我在团队里经常说一句话:你要判断自己该不该用抽象工厂,先问自己,我是不是有一组对象必须以"套"为单位出现?如果我只需要单独生产一种产品,用工厂方法;如果我要的是"一套配合好的产品",这就是抽象工厂的领地。
2.3 回到问题本质:谁在变化,谁在保持稳定
设计模式的本质是"封装变化"。抽象工厂模式里,变化的维度有两个:
- 产品族(主题):蓝主题、金主题,未来可能加粉主题、夜主题——这是"横向维度"。
- 产品等级(控件种类):按钮、输入框、日期选择器,未来可能加下拉框、弹窗——这是"纵向维度"。
抽象工厂模式的结构,相当于在二维坐标系里建立一个网格:每个具体工厂负责一个产品族(一行),每个工厂里的方法负责生产该族下的一种产品(一列)。当你增加一个产品族时,只需要新增一行(新工厂),所有已有代码不需要改动;当你增加一个产品等级时,就需要动到所有工厂——因为每个工厂都得新增一个方法。
这个特性决定了抽象工厂模式"对扩展产品族友好,对增加产品等级不友好"。很多书会把这个说成"抽象工厂模式的缺点",但我不这样看。这根本不是缺点,而是结构本身的取舍。任何模式都有适用边界,你把"新增产品等级"的场景硬套进抽象工厂,用错工具当然会觉得它别扭。
3. 抽象工厂模式的四大角色拆解:把类图翻译成人话
3.1 角色的名字和职责
抽象工厂模式有四个核心角色,我先把它们列出来,然后逐个用大白话解释。
| 角色 | 名称 | 职责 |
|---|---|---|
| AbstractFactory | 抽象工厂 | 定义生产产品族的接口,通常每个工厂方法返回一种抽象产品 |
| ConcreteFactory | 具体工厂 | 实现抽象工厂,生产一组具体的产品族 |
| AbstractProduct | 抽象产品 | 定义产品的公共接口,比如Button接口 |
| ConcreteProduct | 具体产品 | 实现抽象产品接口,比如BlueButton、GoldButton |
画成类图就是一张很经典的图:抽象工厂在顶部,指向多个抽象产品;具体工厂在左下角,实现抽象工厂的同时连接到具体产品;客户端持有抽象工厂和抽象产品,不关心具体实现。
3.2 一个能直接跑的最小示例:主题控件工厂
我不画UML了,直接上代码,这个例子你拿过去就能跑,把它跑通之后再回去看书上的类图,一眼就懂。
先定义产品接口:
// 抽象产品:按钮 public interface Button { void render(); void click(); } // 抽象产品:输入框 public interface TextField { void render(); void setPlaceholder(String text); } // 抽象产品:日期选择器 public interface DatePicker { void render(); void setRange(Date start, Date end); }然后实现蓝色主题的具体产品:
public class BlueButton implements Button { @Override public void render() { System.out.println("渲染蓝色主题按钮"); } @Override public void click() { System.out.println("蓝色按钮点击反馈"); } } public class BlueTextField implements TextField { @Override public void render() { System.out.println("渲染蓝色主题输入框"); } @Override public void setPlaceholder(String text) { System.out.println("蓝色输入框占位符: " + text); } } public class BlueDatePicker implements DatePicker { @Override public void render() { System.out.println("渲染蓝色主题单面板日期选择器"); } @Override public void setRange(Date start, Date end) { System.out.println("蓝色日期范围: " + start + " - " + end); } }金色主题的类似,我就不重复贴了,你按同样的方式写一个GoldButton、GoldTextField、GoldDatePicker。接下来是抽象工厂和具体工厂:
// 抽象工厂 public interface UIFactory { Button createButton(); TextField createTextField(); DatePicker createDatePicker(); } // 具体工厂:蓝色主题工厂 public class BlueUIFactory implements UIFactory { @Override public Button createButton() { return new BlueButton(); } @Override public TextField createTextField() { return new BlueTextField(); } @Override public DatePicker createDatePicker() { return new BlueDatePicker(); } } // 具体工厂:金色主题工厂 public class GoldUIFactory implements UIFactory { @Override public Button createButton() { return new GoldButton(); } @Override public TextField createTextField() { return new GoldTextField(); } @Override public DatePicker createDatePicker() { return new GoldDatePicker(); } }最后是客户端代码,加入了简单的工厂选择逻辑:
public class Application { private Button button; private TextField textField; private DatePicker datePicker; public Application(UIFactory factory) { // 客户端只依赖抽象工厂,不依赖任何具体类 this.button = factory.createButton(); this.textField = factory.createTextField(); this.datePicker = factory.createDatePicker(); } public void render() { button.render(); textField.render(); datePicker.render(); } public static void main(String[] args) { // 运行时决定使用哪个工厂 UIFactory factory = config("gold"); Application app = new Application(factory); app.render(); } private static UIFactory config(String theme) { if ("blue".equals(theme)) { return new BlueUIFactory(); } else if ("gold".equals(theme)) { return new GoldUIFactory(); } throw new IllegalArgumentException("未知主题: " + theme); } }注意看,在Application类里,button、textField、datePicker的声明类型都是抽象接口,构造函数接收的也是UIFactory接口。这就是抽象工厂模式的核心价值:客户端代码和具体产品完全解耦,它甚至都不知道BlueButton这个类存在。全局只有一个config方法里出现了具体工厂的名字,以后要加粉色主题,只需要写一个PinkUIFactory,然后在config里加一行分支,全部搞定。
3.3 客户端为什么可以做到"完全不懂产品细节"
有人会问:客户端只拿到了抽象产品接口,那它调用button.render()的时候,JVM怎么知道到底该执行BlueButton.render()还是GoldButton.render()?
这就涉及Java的多态机制了。工厂返回的虽然是接口类型,但对象本质上是具体类的实例,只是它的"标签"被贴成了接口。运行时调用方法时,JVM会根据对象的实际类型进行动态绑定,找到正确的实现类去执行。
这是Java/C#这类面向对象语言里的常识,但很多初学者会在这里绕不过来。记住一句话:接口类型引用 + 多态方法分派 = 客户端无感知的具体实现替换。抽象工厂模式依赖这个机制,它才能实现"换主题只需换工厂,其他代码一行不动"的效果。
4. 抽象工厂 vs 工厂方法:一张表说清两者的边界
4.1 核心区别不在"方法数量",而在"产品关联性"
很多人会把抽象工厂模式和工厂方法模式搞混,觉得"工厂方法是一个方法创建一种产品,抽象工厂是多个方法创建多种产品"——这只是表面现象。真正的区别在于,抽象工厂创建的一组产品之间有内在关联,必须成套出现。
工厂方法模式的重点在"继承",它让子类决定某个产品怎么创建;抽象工厂模式的重点在"组合",它把多个产品的创建逻辑组合在一个工厂接口里,保证产品族的一致性。
举个电商业务的例子。假设你要生成订单通知消息,通知方式有短信和邮件两种。如果你只需要生成"一条"通知消息,用工厂方法就够:一个MessageFactory,子类SmsMessageFactory和EmailMessageFactory分别生成短信和邮件。但如果你想给VIP用户同时发短信+邮件+站内信三连通知,并且要求这三者的文案、模板、语气都是一套风格,那你就需要抽象工厂——NotificationFactory接口提供createSms()、createEmail()、createSiteMessage()三个方法,VipNotificationFactory实现这三个方法并且保证风格统一。
4.2 各自的使用场景对照
| 对比维度 | 工厂方法模式 | 抽象工厂模式 |
|---|---|---|
| 意图 | 定义一个创建对象的接口,让子类决定实例化哪个类 | 提供一个创建一系列相关对象的接口 |
| 产品数量 | 一种产品 | 一族产品(多种产品) |
| 产品之间的关系 | 无关联,相互独立 | 强关联,必须成套使用 |
| 新增产品族 | 新增一个子类 | 新增一个具体工厂类 |
| 新增产品等级 | 修改抽象工厂接口 | 修改抽象工厂接口 |
| 典型场景 | 日志记录器、数据库连接 | UI主题、跨平台控件、数据库访问层 |
这里需要特别提醒:抽象工厂模式不适合产品之间无关联的场景。如果你只是想让"每次都能创建不同类型的对象",用工厂方法就足够了,硬上抽象工厂只会增加接口的复杂度,让代码显得很臃肿。我在Code Review的时候见过很多人拿抽象工厂套一切,最后接口里十几个方法,实际用到的没几个,这就是过度设计。
4.3 换个角度理解:抽象工厂是工厂方法的"团队版"
如果你用过工厂方法模式,再看抽象工厂,可以把它理解为"将多个工厂方法组织进同一个接口"。抽象工厂里的每个方法,本质上都是一个工厂方法模式的应用。区别只在于,这些工厂方法必须绑定在同一个工厂里,内部共享同一套配置和依赖。
我曾经在一个项目里,用工厂方法模式分别创建了RedisCache、MySQLRepository和KafkaProducer三个组件,每个组件各用各的工厂。结果到了上线前,运维说要拿一套"仿真环境"来测,仿真环境里的Redis是mock的,MySQL是内存库,Kafka是假的。如果我用的是抽象工厂模式,只需要做一个SimulationEnvironmentFactory,把这三个mock组件都生产出来,整个系统切到仿真环境就是一行配置的事。而我当时用的是三个独立的工厂方法,结果改配置改了半个下午,每个组件都单独调一遍。
这个经历后来成了我在团队里讲"产品族"概念时的经典案例。当你发现你的系统里有多个组件必须一起切换时,你有意识地用抽象工厂去收敛它们,是一种非常聪明的架构决策。
5. 深入实战:多数据库支持场景下的抽象工厂落地
5.1 场景设定:一个需要同时支持MySQL和PostgreSQL的报表系统
我做过的另一个实际项目是报表系统,它要支持MySQL和PostgreSQL两种数据库,订单数据、用户数据、报表数据分别访问。刚开始,团队里的做法是在数据访问层写一堆if (dbType == "mysql")的分支,每个查询方法里都判断一遍。到后来需求越来越多,代码里到处是这种判断,改配置都要提心吊胆,生怕漏了一个地方。
后来重构的时候,我引入了抽象工厂模式,效果立竿见影。下面是简化版的实现,你感受一下整个结构。
定义抽象产品接口,代表不同类型的数据访问对象:
public interface OrderDAO { List<Order> queryOrders(); void insertOrder(Order order); } public interface UserDAO { User queryUserById(String userId); void updateUser(User user); }MySQL的实现:
public class MySQLOrderDAO implements OrderDAO { @Override public List<Order> queryOrders() { // MySQL专用SQL实现 System.out.println("执行MySQL订单查询"); return null; } @Override public void insertOrder(Order order) { System.out.println("执行MySQL订单插入"); } } public class MySQLUserDAO implements UserDAO { // MySQL用户DAO实现,省略具体逻辑 }PostgreSQL的实现类似,不重复贴。然后是抽象工厂接口:
public interface DAOFactory { OrderDAO createOrderDAO(); UserDAO createUserDAO(); }两个具体工厂:
public class MySQLDAOFactory implements DAOFactory { @Override public OrderDAO createOrderDAO() { return new MySQLOrderDAO(); } @Override public UserDAO createUserDAO() { return new MySQLUserDAO(); } } public class PostgreSQLDAOFactory implements DAOFactory { @Override public OrderDAO createOrderDAO() { return new PostgreSQLOrderDAO(); } @Override public UserDAO createUserDAO() { return new PostgreSQLUserDAO(); } }客户端代码:
public class ReportService { private final OrderDAO orderDAO; private final UserDAO userDAO; public ReportService(DAOFactory factory) { this.orderDAO = factory.createOrderDAO(); this.userDAO = factory.createUserDAO(); } public void generateReport() { List<Order> orders = orderDAO.queryOrders(); // 逻辑处理 } }这个设计的好处一眼就能看出来:以后要加Oracle支持,新建一个OracleDAOFactory,把OracleOrderDAO、OracleUserDAO写出来,然后在装配层把工厂换成OracleDAOFactory,整个系统就能跑。原有的ReportService代码一行都不用改,测试也不用重写。
5.2 工厂创建时机:启动时装配 vs 每次使用时获取
刚才代码里,ReportService的构造函数要求传入一个DAOFactory。那么问题来了:这个工厂是谁创建的?在什么时机创建的?
两种常见做法:
做法一:系统启动时装配(推荐)
在应用启动的地方,根据配置读取数据库类型,创建一次性工厂,然后通过依赖注入把工厂传给各个Service。
DAOFactory factory = createFactoryByConfig(); ReportService reportService = new ReportService(factory);做法二:每个Service自己判断(不推荐)
在Service内部根据配置文件判断数据库类型,自己创建工厂。这个做法会把工厂选择和Service逻辑耦合在一起,违背了抽象工厂的初衷。
我在项目中实测下来,推荐做法的好处是:切换数据库只需要改配置文件,重启应用,全部组件统一切换。因为工厂本身是无状态的,你可以安全地把它做成单例,不会产生并发问题。
5.3 配置驱动:把"选择工厂"这件事交给Spring
在Spring项目中,我们可以把工厂选择做成配置驱动,非常干净。先在配置里声明三个Bean:
@Configuration public class DatabaseConfig { @Value("${app.db.type}") private String dbType; @Bean public DAOFactory daoFactory() { if ("mysql".equals(dbType)) { return new MySQLDAOFactory(); } else if ("postgresql".equals(dbType)) { return new PostgreSQLDAOFactory(); } throw new IllegalArgumentException("Unsupported db type: " + dbType); } }然后在Service里直接注入:
@Service public class ReportService { private final OrderDAO orderDAO; private final UserDAO userDAO; public ReportService(DAOFactory factory) { this.orderDAO = factory.createOrderDAO(); this.userDAO = factory.createUserDAO(); } }切换数据库只需修改application.properties里的一行:
app.db.type=postgresql整个过程,你不再关心MySQLOrderDAO和PostgreSQLOrderDAO到底在哪里,也不用在代码里保留任何数据库类型的判断。这就是抽象工厂模式在真实工程中的落地形态。
6. 抽象工厂模式的扩展性陷阱:变的是工厂,变不得的是接口
6.1 产品族扩展容易,产品等级扩展昂贵
前面提到过,抽象工厂模式对"增加产品族"友好——新增主题、新增数据库,都只需要新增具体工厂类。但对"增加产品等级"非常不友好:每增加一种产品接口,你必须在抽象工厂接口和所有具体工厂里各加一个方法。
假设你的UI系统要增加一个"下拉框"控件,改动的清单是这样的:
- 新增
ComboBox抽象产品接口; - 在
UIFactory接口新增createComboBox()方法; - 在
BlueUIFactory新增BlueComboBox实现; - 在
GoldUIFactory新增GoldComboBox实现。
如果已经接入了十个主题工厂,那就是一个接口+十个实现类+一个抽象产品接口的改动量。这样的工作量说大不大,说小也不小。关键是,每改动一次抽象工厂接口,所有依赖它的代码都要编译,任何没有实现新方法的工厂都会报错——这既是约束,也是保护。
6.2 典型应对方案:默认实现兜底
如果产品等级扩展概率比较高,可以用"默认实现"来兜底。例如在接口里加一个default方法(Java 8之后支持),返回一个通用的默认控件,这样旧工厂可以不用改动,直接继承默认行为。
public interface UIFactory { Button createButton(); TextField createTextField(); DatePicker createDatePicker(); // 默认实现兜底,新工厂可以重写 default ComboBox createComboBox() { return new DefaultComboBox(); } }这个技巧在渐进式重构里非常有用。你不需要一次把所有工厂都改完,可以先提供默认实现,让系统能编译,然后逐个工厂覆盖为真正的定制实现。我习惯把这种方法叫作"平滑扩展",它能让团队避免"一次改动牵动全局"的阵痛。
6.3 什么时候不该用抽象工厂模式
我得泼一盆冷水:不是所有多产品场景都适合抽象工厂。以下情况请谨慎使用:
- 产品之间没有关联性。比如你只想要一个能创建随机形状的工厂,圆是圆、方是方,彼此不搭界,用工厂方法或简单工厂就够了。
- 产品等级经常变化。如果你的系统几乎每个月都要加一种新产品类型,抽象工厂要求你修改所有工厂,这个成本你可能受不了。
- 产品族维度单一。如果整个系统只有一种产品族,搞抽象工厂就是多此一举。何必为了一个永远只有一套实现的接口增加抽象层?
- 团队成员设计模式水平参差不齐。这个理由可能有点"政治不正确",但却是现实中的大问题。抽象工厂模式引入的抽象层级比较多,如果团队里有人不理解它的设计意图,很容易把它用歪,最后代码比不用设计模式还要难维护。
6.4 模式不是银弹,但组合使用更强大
抽象工厂模式经常和其他模式组合使用。比如它和单例模式结合,可以保证每个具体工厂在整个应用中只有一个实例;它和工厂方法模式结合,可以在一套抽象产品族内部再做一层产品细节的创建隔离;它和原型模式结合,可以用克隆对象替代工厂创建,减少构造开销。
在实际代码中,这些模式不会像教科书那样"纯净"地出现。你看到一段代码,很可能既是抽象工厂,又用了单例,还夹杂了点策略模式的思想。这很正常,设计模式本来就不是僵化的教条,而是一套解决问题的工具箱。关键不在于你用了哪个模式的名字,而在于你的代码是否真正做到了高内聚、低耦合。
7. 测试策略与踩坑笔记:抽象工厂模式不是银弹
7.1 单测里怎么用抽象工厂替换真实实现
抽象工厂模式的另一个巨大优势是测试友好。以前面报表系统为例,它依赖DAO,但你在单元测试里并不想连真数据库。这时候,只需要手写一个MockDAOFactory,配上内存数据库或Mock对象,就能把ReportService的依赖全部替换掉。
public class MockDAOFactory implements DAOFactory { @Override public OrderDAO createOrderDAO() { return new MockOrderDAO(); } @Override public UserDAO createUserDAO() { return new MockUserDAO(); } }测试代码:
public class ReportServiceTest { @Test public void testGenerateReport() { ReportService service = new ReportService(new MockDAOFactory()); // 断言相关的行为 } }你会发现,测试代码不需要任何Mockito之类的工具,也能写出很干净的单测。因为抽象工厂模式和依赖注入天生搭配,你只需要在测试装配层把工厂换掉即可。
7.2 我踩过的坑:工厂方法返回类型用了具体类
有一次重构代码,某个同事把工厂方法的返回类型直接写成了具体类,比如:
public BlueButton createButton();当时看着没毛病,反正工厂就是蓝色主题的工厂。但后来要增加一个"银色主题"时,这个工厂没法复用,因为返回类型被焊死在BlueButton了。抽象工厂模式下,所有工厂方法的返回类型必须是抽象产品接口,这是红线。一旦你返回具体类型,客户端代码就直接依赖于具体实现,整个模式就崩塌了。
我后来在团队Code Review里定了一条规矩:凡是工厂返回类型,一律不允许返回具体类,谁写了具体类,Review直接打回。这条规矩实施之后,抽象工厂相关的代码质量明显提升。
7.3 另一个坑:多个工厂方法共用可变状态
当建立BlueUIFactory时,你可能会在字段里保存一个共享的配置对象。如果这个工厂同时被多个线程调用,字段被并发修改,就会出现线程安全问题。比如:
public class BlueUIFactory implements UIFactory { private String themeConfig; // 共享可变状态 ... }解决思路很简单:工厂最好是无状态的。如果需要传递配置,在工厂方法参数里显式传入,而不是存在字段里。实在要持有状态,就给工厂加锁或者使用ThreadLocal,但后者会让代码复杂度上升,不是首选。我在实战中坚持"无状态工厂"原则,多个Service可以安全共享同一个工厂实例,极大简化了并发设计。
7.4 抽象工厂模式的调试技巧
调试抽象工厂代码时,有个很实用的技巧:在工厂方法里打日志,或者用调试器观察返回对象的实际类型。因为客户端看到的是抽象接口,很容易忽略对象底层是哪个具体实现,导致查问题的时候一头雾水。打印日志是一个快速定位的思路:
System.out.println("订单DAO实例: " + orderDAO.getClass().getSimpleName());输出MySQLOrderDAO,你就能确认客户端确实拿到了正确工厂的产品。这个技巧对排查"配置改了但没生效"这种问题尤其好用。我曾经就因为工厂Bean创建时机不对,导致配置已经切到PostgreSQL,但客户端一直拿的是MySQL的DAO,排查了好久,最后靠打印对象类型才发现问题。
8. 延伸场景:从跨平台UI到中间件适配的抽象工厂
8.1 跨平台UI库:Windows、macOS、Linux三端一致体验
除了主题系统和数据库系统,抽象工厂模式在跨平台UI库中也非常常见。你看Java的AWT,或者JavaFX内部的控件工厂设计,它们都需要根据操作系统决定渲染行为。如果我写一个跨平台应用,需要处理滚动条、按钮、菜单栏,我可以定义:
public interface PlatformWidgetFactory { ScrollBar createScrollBar(); MenuBar createMenuBar(); Button createButton(); }Windows工厂:
public class WindowsWidgetFactory implements PlatformWidgetFactory { @Override public ScrollBar createScrollBar() { return new WindowsScrollBar(); } @Override public MenuBar createMenuBar() { return new WindowsMenuBar(); } @Override public Button createButton() { return new WindowsButton(); } }macOS工厂和Linux工厂同理。客户端完全不需要知道当前运行在什么平台上,只要在启动时根据操作系统选一个工厂,剩下的代码全部面对抽象接口。这样写出来的跨平台程序,渲染行为高度一致,不会出现"Windows上按钮圆角,macOS上按钮直角"的不协调。
8.2 中间件适配:Kafka、RabbitMQ、RocketMQ的消息产品族
假设公司要做一个统一的消息推送平台,底层可以接Kafka、RabbitMQ、RocketMQ三种消息队列,但业务方不需要关心具体是哪一种。消息产品的抽象接口可以是:
public interface MessageProducer { void publish(String topic, String payload); } public interface MessageConsumer { void subscribe(String topic, ConsumerRecordHandler handler); }具体产品:
public class KafkaProducer implements MessageProducer { ... } public class RabbitMQProducer implements MessageProducer { ... }工厂接口:
public interface MiddlewareFactory { MessageProducer createProducer(); MessageConsumer createConsumer(); }有了这个抽象,业务代码只需要:
public class NotificationService { private final MessageProducer producer; private final MessageConsumer consumer; public NotificationService(MiddlewareFactory factory) { this.producer = factory.createProducer(); this.consumer = factory.createConsumer(); } }将来要把底层从Kafka切到RocketMQ,只需新建一个RocketMQFactory,然后在配置装配层替换Bean。整个业务代码零改动。这就是抽象工厂在中间件适配层面的典型应用。
8.3 抽象工厂模式的兄弟模式:Builder模式的区别
有人会把抽象工厂模式和Builder模式混为一谈,因为二者都涉及"复杂对象创建"。但它们有本质区别:
- 抽象工厂:关注产品族的创建,多个产品接口统一在一个工厂下。
- Builder模式:关注单个复杂对象的逐步构建,比如一个拥有几十个可选配置的
ReportConfig对象。
如果你要创建的是一个有大量可选参数的复杂对象,用Builder;如果你要创建的是"一族相互配合的对象",用抽象工厂。两者并不冲突,可以嵌套使用——抽象工厂里的某个具体产品,内部可能用Builder来组装。
9. 最后想说的:抽象工厂模式的内功心法
抽象工厂模式的代码量其实不多,但它的设计思想值得反复咀嚼。我把整个核心总结成三句"内功心法":
- 面向抽象编程,绝不面向具体类编程。客户端只依赖
UIFactory、Button、TextField,永远不出现BlueButton的名字,这就是核心。 - 产品族成套出现,工厂保证一致性。要让系统里替换一个"套"的对象,比替换"单个"对象更彻底——换工厂,全家换。
- 扩展的开关,永远在配置和装配层。你不需要在业务代码里塞满
if判断,只需要在应用启动时或依赖注入处,基于配置文件选择一个工厂。
如果你要深入学,我建议你亲手做这三件事:
- 第一步,把文章里的主题控件例子从零敲一遍,运行起来,观察输出变化;
- 第二步,新增一个"粉色主题"的工厂,看看你需要改多少代码——你会发现只需要加一个新类和一个分支,其他代码不用动;
- 第三步,再新增一种"下拉框"控件,感受一下所有工厂都要新增方法的约束,理解模式适用边界的意义。
我第一次接触抽象工厂模式时,真的看懂了类图,但始终体会不到它的价值。直到有一次,在一个多数据库项目里,因为需求变更,需要紧急支持第三种数据库缓存系统,我写了一个新的工厂类,然后在配置文件里改了一行,整个系统就跑起来了。那一刻,我一下子明白了什么叫"为扩展而生"。希望你也能在某个项目里,感受到这种酣畅淋漓的替换体验。好的架构不是炫技,而是让未来的人,包括未来的自己,改起代码来更轻松。