很久没有写技术文章了,今天写一篇技术文章,也是java当中比较重要用的比较多的一种设计模式。
大家已经掌握了用于创建单个对象的工厂方法模式。但要是系统需要创建一整套相互关联或相互依赖的对象,该怎么处理呢?要是为每个对象都用单独的工厂,可能会出现不兼容的组合,就好比现代风格的椅子搭配维多利亚风格的咖啡桌,显得很不协调。
抽象工厂设计模式把具有共同主题的工厂组合起来,从而解决了这个问题。这就像走进一个专门的展厅,里面所有东西的风格都是相互搭配的。下面我们就来看看它是如何保证代码中各个对象之间协调一致的。
什么是抽象工厂模式?
抽象工厂是一种创建型设计模式,它能让你生成相互关联的对象系列,而且不需要指定它们的具体类。它提供了一个接口,专门用于创建相互依赖或相关的对象系列。
简单来讲,它就是一个 "工厂的工厂"。有一个顶级的抽象工厂接口,而这个工厂的每个具体实现都知道如何为特定的系列或变体创建所有对象。
为什么要使用抽象工厂模式?(它能解决的问题)
使用抽象工厂模式的原因如下:
简单类比:家具店
想象一下,你正在为新家添置家具。你希望所有家具都符合特定的风格,比如现代风格或者维多利亚风格。
作为客户,你不会为了每一件家具去不同的作坊。你根据想要的风格选择一个展厅(工厂),这个展厅会为你提供所有的家具,确保它们都相互匹配。
何时使用它与工厂方法模式的对比
这是一个关键的区别。
工厂方法模式:和继承相关。它专注于创建单个对象,同时让子类决定具体的类。产品的选择嵌入在子类层次结构中。
抽象工厂模式:和组合相关。它专注于创建相互关联的对象系列。产品系列的选择通常由客户端在运行时通过选择要实例化的具体工厂来决定。具体的抽象工厂内部通常会使用工厂方法来创建单个产品。
现实世界中的软件示例
抽象工厂模式在现实软件设计中的常见用例
客户端代码只需使用工厂来获取相应的序列化器和反序列化器,而无需知道底层数据格式的细节。这使应用程序更灵活,并且可以轻松支持新格式,而无需修改核心逻辑。
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 {
public void sitOn() {
System.out.println("坐在时尚的现代椅子上。");
}
public String getStyle() {
return "现代";
}
}
public record ModernSofa() implements Sofa {
public void loungeOn() {
System.out.println("躺在低调的现代沙发上。");
}
public String getStyle() {
return "现代";
}
}
// 维多利亚系列
public record VictorianChair() implements Chair {
public void sitOn() {
System.out.println("坐在华丽的维多利亚风格椅子上。");
}
public String getStyle() {
return "维多利亚";
}
}
public record VictorianSofa() implements Sofa {
public void loungeOn() {
System.out.println("躺在奢华的维多利亚风格沙发上。");
}
public String getStyle() {
return "维多利亚";
}
}
步骤 3:抽象工厂
public interface FurnitureFactory {
Chair createChair();
Sofa createSofa();
// 可以添加 createTable()、createLamp() 等方法
}
步骤 4:具体工厂
public class ModernFurnitureFactory implements FurnitureFactory {
public Chair createChair() {
return new ModernChair();
}
public Sofa createSofa() {
return new ModernSofa();
}
}
public class VictorianFurnitureFactory implements FurnitureFactory {
public Chair createChair() {
return new VictorianChair();
}
public 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();
}
}
输出结果
用现代风格的家具装饰房间:
坐在时尚的现代椅子上。
躺在低调的现代沙发上。
常见陷阱和最佳实践
它与其他模式的关系
使用 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 {
@Override
public String style() {
return "现代";
}
@Override
public void sitOn() {
System.out.println("坐在时尚的" + style() + "椅子上。");
}
}
// 最终 record:代表维多利亚系列中的产品
public record VictorianChair() implements Chair {
@Override
public String style() {
return "维多利亚";
}
@Override
public void sitOn() {
System.out.println("坐在华丽的" + style() + "椅子上。");
}
}
// 沙发系列的密封接口
public sealed interface Sofa permits ModernSofa, VictorianSofa {
String style();
void loungeOn();
}
public record ModernSofa() implements Sofa {
@Override
public String style() {
return "现代";
}
@Override
public void loungeOn() {
System.out.println("躺在低调的" + style() + "沙发上。");
}
}
public record VictorianSofa() implements Sofa {
@Override
public String style() {
return "维多利亚";
}
@Override
public 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 {
public Chair createChair() {
return new ModernChair();
}
public Sofa createSofa() {
return new ModernSofa();
}
}
// 维多利亚系列的最终具体工厂
public final class VictorianFactory implements FurnitureFactory {
public Chair createChair() {
return new VictorianChair();
}
public Sofa createSofa() {
return new VictorianSofa();
}
}
步骤 4:客户端代码(使用 Java 21 的 switch 表达式)
客户端代码受益于密封层次结构的 exhaustive 检查。编译器知道 FurnitureFactory 的所有可能类型,使得 switch 表达式非常安全,如果覆盖了所有情况,则不需要默认子句。
public class InteriorDesigner {
private final Chair chair;
private final Sofa sofa;
// 客户端只依赖于抽象
public InteriorDesigner(FurnitureFactory factory) {
chair = factory.createChair();</doubaocanvas>
附:诚信为人,认真做事,人在做,天在看。每日更新一篇关于技术或者国外lead文章长期坚持原创不易,如文章引起大家共鸣请大家关注,点赞,转发,以支持勤于奋继续分析创作。
欢迎访问勤于奋公众号,本公众号上的所有内容,包括文字、图像、链接等,均为个人意见和知识分享之用,不应被视为专业意见。
在采取任何基于本公众号内容的行动之前,我们建议您进行独立的研究和/或咨询专业人士。
本公众号不保证内容的准确性、有效性、完整性或可靠性,并且不对任何错误、遗漏或结果承担责任。对于本公众号链接到的其他网站内容,本博客亦不承担责任。
本免责声明的最终解释权归勤于奋公众号所有。
没有评论:
发表评论