Java 中的抽象工厂设计模式
通过简单类比理解 Java 中的抽象工厂设计模式
大家已经掌握了用于创建单个对象的工厂方法模式。但要是系统需要创建一整套相互关联或相互依赖的对象,该怎么处理呢?要是为每个对象都用单独的工厂,可能会出现不兼容的组合,就好比现代风格的椅子搭配维多利亚风格的咖啡桌,显得很不协调。
抽象工厂设计模式把具有共同主题的工厂组合起来,从而解决了这个问题。这就像走进一个专门的展厅,里面所有东西的风格都是相互搭配的。下面我们就来看看它是如何保证代码中各个对象之间协调一致的。
什么是抽象工厂模式?
抽象工厂是一种创建型设计模式,它能让你生成相互关联的对象系列,而且不需要指定它们的具体类。它提供了一个接口,专门用于创建相互依赖或相关的对象系列。
简单来讲,它就是一个 “工厂的工厂”。有一个顶级的抽象工厂接口,而这个工厂的每个具体实现都知道如何为特定的系列或变体创建所有对象。
为什么要使用抽象工厂模式?(它能解决的问题)
使用抽象工厂模式的原因如下:
- 确保兼容性:保证一组产品是兼容的,并且是设计成可以一起工作的。避免混合不同系列的产品(比如,macOS 按钮搭配 Windows 滚动条)。
- 支持多种主题或变体:轻松切换整个产品系列。更改工厂,应用程序生成的所有内容都会随之改变(例如,将 UI 主题从 “浅色” 模式切换到 “深色” 模式)。
- 完全解耦客户端代码:客户端代码完全是根据抽象接口(椅子、沙发)来编写的。它不知道具体的类(现代椅子、维多利亚沙发),甚至不知道正在使用哪个系列,这让代码非常稳健。
简单类比:家具店
想象一下,你正在为新家添置家具。你希望所有家具都符合特定的风格,比如现代风格或者维多利亚风格。
- 你(客户端)就是房主。
- 抽象家具工厂是能够创建椅子、沙发和桌子的家具店的目录或概念。
- 具体工厂(现代家具工厂、维多利亚家具工厂)是你去参观的特定展厅。现代展厅只卖现代风格的家具,维多利亚展厅只卖维多利亚风格的家具。
- 抽象产品(椅子、沙发、桌子)是你需要的家具类型。
- 具体产品(现代椅子、维多利亚沙发)是你购买的实际家具。
作为客户,你不会为了每一件家具去不同的作坊。你根据想要的风格选择一个展厅(工厂),这个展厅会为你提供所有的家具,确保它们都相互匹配。
何时使用它与工厂方法模式的对比
这是一个关键的区别。
工厂方法模式:和继承相关。它专注于创建单个对象,同时让子类决定具体的类。产品的选择嵌入在子类层次结构中。
抽象工厂模式:和组合相关。它专注于创建相互关联的对象系列。产品系列的选择通常由客户端在运行时通过选择要实例化的具体工厂来决定。具体的抽象工厂内部通常会使用工厂方法来创建单个产品。
现实世界中的软件示例
- 跨平台 UI 工具包:GUIFactory 接口有 createButton ()、createScrollbar () 和 createMenu () 方法。WindowsFactory 创建 Windows 风格的组件,而 MacFactory 创建 macOS 风格的组件。应用程序使用一个工厂来保证一致性。
- 数据库抽象层:DatabaseFactory 接口具有 createConnection ()、createCommand () 和 createDataAdapter () 方法。MySqlFactory 和 PostgreSqlFactory 创建一整套兼容的数据库访问对象。
- 主题系统:ThemeFactory 具有 createDialog ()、createTooltip () 和 createIcon () 方法。LightThemeFactory 和 DarkThemeFactory 提供各自主题的所有 UI 元素。
抽象工厂模式在现实软件设计中的常见用例
- 跨平台 UI 工具包:这是最经典的例子之一。抽象工厂可用于创建符合特定外观和风格的 UI 组件系列(按钮、菜单、文本字段),例如 Windows、macOS 或自定义 Web 主题。客户端代码只与通用的 Button 接口交互,而具体的工厂会创建正确的平台特定按钮。
- 数据库访问层:该模式可用于为不同的数据库系统创建对象系列。例如,一个工厂可以提供用于连接 MySQL 数据库的对象(MySQLConnection 和 MySQLCommand),或者用于连接 PostgreSQL 数据库的对象(PostgreSQLConnection 和 PostgreSQLCommand)。应用程序的业务逻辑保持独立于所使用的特定数据库。
- 可配置系统:当应用程序需要支持多种配置或产品系列时,抽象工厂是理想的选择。例如,游戏引擎可以有一个 EnemyFactory,它可以生成简单敌人或困难敌人,或者有一个 GraphicsFactory,它可以为高分辨率或低分辨率渲染创建对象。
- 创建具有不同标准或规则的对象:考虑一个生成文档的系统。抽象工厂可用于创建遵循不同法律或行业标准的对象系列(例如,FinancialReportFactory 创建 FinancialReport 和 AuditLog 对象,而 MedicalReportFactory 创建 MedicalReport 和 PatientHistory 对象)。
- 处理多种文件格式:需要从不同文件类型(例如 XML、JSON 或 CSV)导入或导出数据的应用程序可以使用抽象工厂。JsonFactory 会创建 JsonReader 和 JsonWriter,而 XmlFactory 会创建 XmlReader 和 XmlWriter。这使客户端代码不会与特定的文件格式耦合。
- 数据序列化和反序列化:想象一个需要以不同格式(如 XML、JSON 或 YAML)保存和加载用户数据的应用程序。抽象工厂可用于为每种格式创建一组一致的对象。
- JsonFactory 将提供 JsonSerializer 和 JsonDeserializer。
- XmlFactory 将提供 XmlSerializer 和 XmlDeserializer。
客户端代码只需使用工厂来获取相应的序列化器和反序列化器,而无需知道底层数据格式的细节。这使应用程序更灵活,并且可以轻松支持新格式,而无需修改核心逻辑。
Java 中抽象工厂设计模式的实现代码示例(现代 Java 21)
我们使用现代 Java 特性(如 records 和 switch 表达式)来编写家具店的类比代码。
步骤 1:抽象产品
public interface Chair {void sitOn();String getStyle();
}public interface Sofa {void loungeOn();String getStyle();
}
步骤 2:具体产品
如果产品主要用于保存数据,我们可以使用 records 来创建不可变的数据对象。
// 现代系列
public record ModernChair() implements Chair {@Overridepublic void sitOn() {System.out.println("坐在时尚的现代椅子上。");}@Overridepublic String getStyle() { return "现代"; }
}public record ModernSofa() implements Sofa {@Overridepublic void loungeOn() {System.out.println("躺在低调的现代沙发上。");}@Overridepublic String getStyle() { return "现代"; }
}// 维多利亚系列
public record VictorianChair() implements Chair {@Overridepublic void sitOn() {System.out.println("坐在华丽的维多利亚风格椅子上。");}@Overridepublic String getStyle() { return "维多利亚"; }
}public record VictorianSofa() implements Sofa {@Overridepublic void loungeOn() {System.out.println("躺在奢华的维多利亚风格沙发上。");}@Overridepublic String getStyle() { return "维多利亚"; }
}
步骤 3:抽象工厂
public interface FurnitureFactory {Chair createChair();Sofa createSofa();// 可以添加 createTable()、createLamp() 等方法
}
步骤 4:具体工厂
public class ModernFurnitureFactory implements FurnitureFactory { @Overridepublic Chair createChair() {return new ModernChair();}@Overridepublic Sofa createSofa() {return new ModernSofa();}
}public class VictorianFurnitureFactory implements FurnitureFactory {@Overridepublic Chair createChair() {return new VictorianChair();}@Overridepublic Sofa createSofa() {return new VictorianSofa();}
}
步骤 5:客户端如何使用(测试实现)
public class InteriorDesigner {private final Chair chair;private final Sofa sofa;// 客户端与抽象工厂组合。依赖注入的体现!public InteriorDesigner(FurnitureFactory factory) {// 客户端使用抽象接口创建产品系列。// 它完全不知道得到的是哪个具体系列。chair = factory.createChair();sofa = factory.createSofa();}public void decorateRoom() {System.out.println("用" + chair.getStyle() + "风格的家具装饰房间:");chair.sitOn();sofa.loungeOn();}public static void main(String[] args) {// 模拟配置选择String desiredStyle = "modern";// 使用 switch 表达式在运行时选择整个系列FurnitureFactory factory = switch (desiredStyle.toLowerCase()) {case "modern" -> new ModernFurnitureFactory();case "victorian" -> new VictorianFurnitureFactory();default -> throw new IllegalArgumentException("未知的家具风格:" + desiredStyle);};// 客户端代码完全独立于具体的家具类InteriorDesigner designer = new InteriorDesigner(factory);designer.decorateRoom();}
}
输出结果
用现代风格的家具装饰房间:
坐在时尚的现代椅子上。
躺在低调的现代沙发上。
常见陷阱和最佳实践
- 陷阱:万能工厂(God Factory):不要创建一个可以生产所有东西的单一工厂。这违反了单一职责原则。抽象工厂应该专注于单一的产品系列。
- 陷阱:添加新产品:向层次结构中添加新的产品类型(例如灯)需要修改 FurnitureFactory 接口和所有现有的具体工厂。如果你无法控制代码,这可能会很麻烦。
- 最佳实践:与依赖注入(DI)结合使用:该模式与 DI 容器(如 Spring)非常匹配。容器充当具体工厂的可配置来源,然后将其注入到像我们的 InteriorDesigner 这样的客户端中。
- 最佳实践:与工厂方法结合使用:具体工厂内部通常使用工厂方法模式来实例化产品。
它与其他模式的关系
- 工厂方法模式:如前所述,它通常用于实现具体抽象工厂的方法。
- 单例模式:具体工厂类通常实现为单例,因为通常只需要特定工厂的一个实例。
- 建造者模式:抽象工厂返回一系列产品,建造者模式则通过一个单独的建造者对象逐步构建复杂对象。它们解决不同的问题,但可以相互补充。
使用 Java 21 的另一种实现方法
我们使用现代 Java 21 的特性(如密封类型和 records)来实现相同的家具店用例,这些特性能让我们编写更具目的性、更安全和更具表达力的代码。这种实现使用密封接口来定义工厂和产品的封闭层次结构,使用 records 来存储不可变的产品数据,并使用最终类来防止意外扩展。这使得模式的结构更加明确,并经过编译器验证。
步骤 1:定义密封的产品层次结构
我们为 Chair 和 Sofa 使用密封接口,以明确列出所有可能的实现。这告诉编译器(和其他开发人员)没有其他类可以实现这些接口。我们为产品使用 records,因为它们是简单的数据载体。
// 密封接口:只有这两个 records 可以实现 Chair
public sealed interface Chair permits ModernChair, VictorianChair {String style();void sitOn();
}// 最终 record:代表现代系列中的产品
public record ModernChair() implements Chair {@Overridepublic String style() { return "现代"; }@Overridepublic void sitOn() {System.out.println("坐在时尚的" + style() + "椅子上。");}
}// 最终 record:代表维多利亚系列中的产品
public record VictorianChair() implements Chair {@Overridepublic String style() { return "维多利亚"; }@Overridepublic void sitOn() {System.out.println("坐在华丽的" + style() + "椅子上。");}
}// 沙发系列的密封接口
public sealed interface Sofa permits ModernSofa, VictorianSofa {String style();void loungeOn();
}public record ModernSofa() implements Sofa {@Overridepublic String style() { return "现代"; }@Overridepublic void loungeOn() {System.out.println("躺在低调的" + style() + "沙发上。");}
}public record VictorianSofa() implements Sofa {@Overridepublic String style() { return "维多利亚"; }@Overridepublic void loungeOn() {System.out.println("躺在奢华的" + style() + "沙发上。");}
}
步骤 2:定义密封的工厂层次结构
FurnitureFactory 也是一个密封接口。这是一个强大的增强:这意味着我们可以明确列出系统中所有可能的主题变体(例如现代和维多利亚)。添加新的主题(如 ArtDecoFactory)需要有意识地修改 permits 子句。
// 密封接口:抽象工厂。
// 只允许这两个工厂
public sealed interface FurnitureFactory permits ModernFactory, VictorianFactory { // 系列中每个产品的工厂方法Chair createChair();Sofa createSofa();
}
步骤 3:实现最终的具体工厂
我们的具体工厂是最终类,防止任何人对其进行子类化,从而可能破坏预期的系列分组。
// 现代系列的最终具体工厂
public final class ModernFactory implements FurnitureFactory {@Overridepublic Chair createChair() {return new ModernChair();}@Overridepublic Sofa createSofa() {return new ModernSofa();}
}// 维多利亚系列的最终具体工厂
public final class VictorianFactory implements FurnitureFactory {@Overridepublic Chair createChair() {return new VictorianChair();}@Overridepublic Sofa createSofa() {return new VictorianSofa();}
}
步骤 4:客户端代码(使用 Java 21 的 switch 表达式)
客户端代码受益于密封层次结构的 exhaustive 检查。编译器知道 FurnitureFactory 的所有可能类型,使得 switch 表达式非常安全,如果覆盖了所有情况,则不需要默认子句。
java
public class InteriorDesigner {private final Chair chair;private final Sofa sofa;// 客户端只依赖于抽象public InteriorDesigner(FurnitureFactory factory) {chair = factory.createChair();sofa = factory.createSofa();}public void decorateRoom() {System.out.println("房间采用" + chair.style() + "风格布置:");chair.sitOn();sofa.loungeOn();}public static void main(String[] args) {String config = "modern"; // 可以来自配置文件// 编译器检查是否覆盖了所有允许的类型!FurnitureFactory factory = switch (config.toLowerCase()) {case "modern" -> new ModernFactory();case "victorian" -> new VictorianFactory();default -> throw new IllegalArgumentException("意外值:" + config.toLowerCase());};InteriorDesigner client = new InteriorDesigner(factory);client.decorateRoom();}
}
输出结果:
房间采用现代风格布置:
坐在时尚的现代椅子上。
躺在低调的现代沙发上。
这种方法的好处
- 编译器强制执行的架构:sealed 和 final 关键字使模式的结构具有明确的目的性,并防止意外扩展。编译器确保所有可能的工厂和产品都被知晓和处理。
- 不可变性和