04|抽象工厂:一次切换相互匹配的产品族
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.abstractfactory。
系列导航:Java 25 设计模式学习指南
博客后台支持浅色和深色主题,每个主题都有按钮和横幅。分别调用按钮工厂、横幅工厂时,很容易给深色页面装上浅色按钮。组件各自都能工作,但页面整体不一致。
产品族意味着若干种不同组件必须一起遵守同一个系列约定。这里选择主题就是选择一组匹配的产品,而不是只选择一个产品的具体类。
从问题提炼设计意图
抽象工厂提供创建一组相关产品的接口,使客户端能够整体切换产品族,而不直接依赖各产品的具体类。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
ThemeFactory |
抽象工厂,声明按钮和横幅两个创建方法 |
LightThemeFactory、DarkThemeFactory |
具体工厂,分别创建一个主题族 |
Button、Banner |
两类抽象产品 |
LightButton、LightBanner、DarkButton、DarkBanner |
两个产品族中的四个具体产品 |
classDiagram
ThemeFactory <|.. LightThemeFactory
ThemeFactory <|.. DarkThemeFactory
Button <|.. DarkButton
Banner <|.. DarkBanner
DarkThemeFactory ..> DarkButton : creates
DarkThemeFactory ..> DarkBanner : creates
代码思路:变化应该落在哪个对象上
先找两条轴:产品种类是 Button 与 Banner,产品族是 Light 与 Dark。ThemeFactory 的方法对应产品种类,具体工厂对应产品族。画出这张二维表,比记住“工厂的工厂”更有帮助。
调用方只保存一个 ThemeFactory 引用,从它取得所有需要协作的组件。DarkThemeFactory 的两个方法分别创建 DarkButton 和 DarkBanner;共同的工厂来源落实了本例的主题一致性。
为了突出产品族,组件只返回 theme 名称,没有把 DOM、颜色系统和 UI 框架混入示例。真实组件会提供 render、layout 等行为,但仍应围绕调用方需要的能力设计接口。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.abstractfactory,源码目录为 src/main/java/com/hanserwei/patterns/abstractfactory/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
ThemeFactory.java:
package com.hanserwei.patterns.abstractfactory;
/** 一次选择一整族相互匹配的主题组件. */
public interface ThemeFactory {
/** 创建主题按钮. */
Button createButton();
/** 创建同主题横幅. */
Banner createBanner();
}
DarkThemeFactory.java:
package com.hanserwei.patterns.abstractfactory;
/** 保证两个产品属于同一主题的具体工厂. */
public final class DarkThemeFactory implements ThemeFactory {
/** 创建本主题按钮. */
@Override
public Button createButton() {
return new DarkButton();
}
/** 创建本主题横幅. */
@Override
public Banner createBanner() {
return new DarkBanner();
}
}
DarkButton.java:
package com.hanserwei.patterns.abstractfactory;
/** 主题族内的具体组件. */
public final class DarkButton implements Button {
/** 返回该组件所属主题. */
@Override
public String theme() {
return "dark";
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.abstractfactory;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
ThemeFactory factory = new DarkThemeFactory();
System.out.println(factory.createButton().theme() + "/" + factory.createBanner().theme());
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runAbstractfactory
./gradlew test --tests 'com.hanserwei.patterns.abstractfactory.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
dark/dark
用测试确认模式的行为
每个工厂创建的按钮与横幅主题一致;切换工厂同时改变两个组件。测试应覆盖全部工厂,而不只验证深色工厂。
对应测试位于 src/test/java/com/hanserwei/patterns/abstractfactory/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 同一设计系统中的一组 UI 组件。
- 不同存储后端各自提供匹配的连接、事务与命令对象。
- 面向多个平台时,整体替换文件、窗口或输入设备的实现族。
适用边界与容易踩的坑
增加新主题比较顺畅:为各产品种类提供新实现,再增加一个主题工厂。增加新产品种类则代价相反:例如新增 Dialog,ThemeFactory 及所有主题工厂都需要扩展。这是抽象工厂最重要的变化方向判断。
接口本身不会禁止具体工厂错误地混搭产品。本例用家族一致性测试守住约定;复杂系统还可能通过类型参数或受控构造强化限制。不要把“使用了接口”当作已经证明兼容性。
客户端仍可绕过工厂直接 new 组件,所以架构约束需要清晰的装配入口。在不需要产品族一致性的场景,多个独立工厂或直接依赖注入可能更简单。
与相近模式比较
工厂方法通常以一个创建步骤为中心;抽象工厂以一整族创建能力为中心。抽象工厂的实现内部也可以使用工厂方法,但两者的设计意图不同。
动手练习
先增加 HighContrast 产品族,记录需要新增哪些类。再增加 Dialog 产品种类,记录必须修改哪些已有接口和类。用这两份改动清单解释抽象工厂的扩展优势与成本。
系列导航:Java 25 设计模式学习指南