设计模式面试题
更新: 8/14/2025 字数: 0 字 时长: 0 分钟
单例模式有哪几种实现?如何保证线程安全?
想象一下,你家里只有一间茅厕(厕所),每次只能一个人进去用。当有人想用的时候,就得去排队,或者看看里面有没有人。用完了,门开着,下一个人才能进去。
单例模式就是这个意思:保证一个类在整个程序运行过程中,只有一个实例(对象)存在,而且提供一个公共的、方便的方法让大家都能访问到这个唯一的实例。
为啥要这样?比如,你家就一个管家,所有需要他做的事情都找他。或者说,程序里有一个配置中心,所有地方都得从这同一个地方读取配置。
有哪几种实现?
饿汉式(Eager Initialization):
比喻:就像你还没饿,但妈已经把饭做好了放在桌上。等你一说“饿了”,饭马上就能吃。
怎么实现:在类加载的时候,就直接把唯一的实例创建好。
代码大概长这样:
javapublic class Singleton { private static Singleton instance = new Singleton(); // 类加载时就创建了 private Singleton() {} // 构造器私有,外面不能随便 new public static Singleton getInstance() { // 提供一个公共方法获取实例 return instance; } }
优点:简单粗暴,线程安全(因为类加载时就创建了,后面不会再有并发问题)。
缺点:可能会提前创建实例,如果这个实例很占资源,但又一直没用到,就有点浪费。
懒汉式(Lazy Initialization):
比喻:你说“饿了”,妈才开始给你做饭。如果一直没说饿,饭就一直不烧。
怎么实现:第一次需要用到这个实例的时候,才去创建它。
代码大概长这样(初始版本,有线程安全问题):
javapublic class Singleton { private static Singleton instance; // 默认是 null private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次用才创建 instance = new Singleton(); } return instance; } }
优点:真正需要的时候才创建,节省资源。
缺点:有线程安全问题!(看下面怎么解决)
双重检查锁定(Double-Checked Locking - DCL):
比喻:你说“饿了”,妈先看冰箱有没有菜。没菜才去买菜做饭,而且为了避免你和弟弟同时说饿都跑去买菜,她会锁上门(同步),一个人去买。买完菜回来,再看看冰箱里是不是已经有菜了(双重检查),确认没有重复买。
怎么实现:在懒汉式的基础上,加锁来保证线程安全,并且进行两次
null
检查,提高性能。代码大概长这样:
javapublic class Singleton { private volatile static Singleton instance; // 注意这里的 volatile 关键字 private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查,如果已经创建了,就直接返回,避免进入同步块 synchronized (Singleton.class) { // 进入同步块,保证同一时间只有一个线程执行 if (instance == null) { // 第二次检查,防止多个线程同时通过第一次检查,导致重复创建 instance = new Singleton(); } } } return instance; } }
优点:既实现了懒加载,又保证了线程安全,而且性能较好(大部分时间不会进入同步块)。
缺点:实现相对复杂一点点,
volatile
关键字很重要,不加可能会有问题(指令重排)。
静态内部类(Static Inner Class):
比喻:妈把饭店的菜单放在一个小抽屉里,只有你真正想点菜的时候,才去打开抽屉,拿出菜单。这个小抽屉(静态内部类)只有在第一次被打开(使用)时,里面的菜(单例实例)才会被“准备好”。
怎么实现:利用了类加载机制。静态内部类只有在被首次使用时才会被加载,它的静态成员(单例实例)也会随之被初始化。
代码大概长这样:
javapublic class Singleton { private Singleton() {} // 静态内部类,只有在第一次调用 getInstance() 时才会被加载 private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
优点:完美结合了懒加载和线程安全,代码也比较简洁,是推荐的实现方式之一。
枚举(Enum):
比喻:枚举天生就是独一无二的,就像你家的门牌号,只有一个,而且是系统自动保证的。
怎么实现:直接定义一个枚举类型,里面就一个枚举值。
代码大概长这样:
javapublic enum Singleton { INSTANCE; // 唯一的实例 public void doSomething() { // 做一些事情 } } // 使用:Singleton.INSTANCE.doSomething();
优点:最简洁、最推荐的方式!不仅线程安全,还能有效防止反射攻击和序列化问题,简直是“万能解药”。
如何保证线程安全?
- 饿汉式:简单粗暴,因为实例在类加载时就已经创建,JVM 会保证这个过程的线程安全。
- 懒汉式(非线程安全):存在问题,多个线程可能同时判断
instance == null
为真,导致创建多个实例。 - 懒汉式(加同步锁):在
getInstance()
方法上加synchronized
关键字,但性能较差,因为每次调用都会加锁。 - 双重检查锁定(DCL):
synchronized
块只在第一次创建实例时才有效,大部分时间不加锁,配合volatile
关键字防止指令重排,兼顾性能和安全。 - 静态内部类:利用 JVM 类加载机制的线程安全性。一个类在首次加载时只会执行一次初始化,这个过程是线程安全的。
- 枚举:JVM 从语言层面就保证了枚举实例的唯一性,天生线程安全,且能避免反射和序列化问题。
什么是策略模式?一般用在什么场景?
想象一下,你要去上班,可以坐地铁、骑共享单车、打的,或者走路。每种方式都代表一种“策略”,它们都能把你送到公司,但具体怎么实现(坐地铁怎么买票,骑车怎么扫码)是不同的。你可以根据当天的天气、时间、心情来选择用哪种方式。
策略模式就是这个意思:定义一系列的算法(策略),将每一个算法封装起来,并使它们可以相互替换。客户端(你)可以根据需要选择不同的策略,而不需要改变使用策略的代码。
它解决的问题是:当一个操作有多种实现方式,并且这些方式在运行时需要灵活切换时。
一般用在哪些场景?
支付方式选择:
- 比如电商网站,用户可以选择支付宝支付、微信支付、银联支付、银行卡支付等。每种支付方式就是一种策略。
- 代码里会有一个“支付上下文”,你告诉它用支付宝,它就知道怎么调用支付宝接口;告诉它用微信,它就知道怎么调用微信接口。
不同排序算法:
- 比如对一堆数据进行排序,你可以选择冒泡排序、快速排序、插入排序等等。每种排序算法就是一种策略。
- 你可以写一个排序工具类,传入不同的“排序策略”,它就能用不同的方法给你排序。
促销活动计算:
- 比如购物满减、打折、买一送一、积分抵扣等,每种促销规则都是一种策略。
- 结算时,根据用户符合的条件,选择对应的策略来计算最终价格。
文件导出/导入格式:
- 导出数据可以选择导出为 CSV、Excel、PDF 等格式,每种格式对应一种导出策略。
- 导入数据可以选择从不同格式的文件导入,每种格式对应一种导入策略。
不同税率计算:
- 针对不同地区或不同商品类型,税率计算方式不同,每种计算方式是一种策略。
核心思想:把“变”的部分(具体的算法实现)封装起来,把“不变”的部分(调用算法的接口)固定下来。这样,当你需要增加新的算法时,只需要增加一个新的策略类,而不需要修改原有的代码。这符合“开闭原则”(对扩展开放,对修改关闭)。
示例代码:
我们以“计算器操作”为例,实现加法、减法、乘法等不同的计算策略。
策略接口(Strategy Interface)
定义一个公共的接口,所有的具体策略都必须实现这个接口。
java// Step 1: 定义策略接口 public interface OperationStrategy { int doOperation(int num1, int num2); }
具体策略类(Concrete Strategy Classes)
实现策略接口,提供具体的算法或行为。
java// Step 2: 实现具体的策略类 - 加法 public class AddOperation implements OperationStrategy { @Override public int doOperation(int num1, int num2) { return num1 + num2; } } // Step 2: 实现具体的策略类 - 减法 public class SubtractOperation implements OperationStrategy { @Override public int doOperation(int num1, int num2) { return num1 - num2; } } // Step 2: 实现具体的策略类 - 乘法 public class MultiplyOperation implements OperationStrategy { @Override public int doOperation(int num1, int num2) { return num1 * num2; } }
上下文类(Context Class)
持有策略接口的引用,客户端通过上下文类来使用具体的策略。
java// Step 3: 创建上下文类 public class CalculatorContext { private OperationStrategy strategy; public CalculatorContext(OperationStrategy strategy) { this.strategy = strategy; } public int executeOperation(int num1, int num2) { return strategy.doOperation(num1, num2); } // 也可以提供一个方法来动态改变策略 public void setStrategy(OperationStrategy strategy) { this.strategy = strategy; } }
客户端(Client)
使用上下文类和具体的策略来执行操作。
java// Step 4: 客户端代码 public class StrategyPatternDemo { public static void main(String[] args) { // 使用加法策略 CalculatorContext context = new CalculatorContext(new AddOperation()); System.out.println("10 + 5 = " + context.executeOperation(10, 5)); // 输出:10 + 5 = 15 // 切换到减法策略 context.setStrategy(new SubtractOperation()); // 或者直接 new CalculatorContext(new SubtractOperation()) System.out.println("10 - 5 = " + context.executeOperation(10, 5)); // 输出:10 - 5 = 5 // 切换到乘法策略 context.setStrategy(new MultiplyOperation()); System.out.println("10 * 5 = " + context.executeOperation(10, 5)); // 输出:10 * 5 = 50 // 也可以直接创建新的上下文 CalculatorContext multiplyContext = new CalculatorContext(new MultiplyOperation()); System.out.println("20 * 3 = " + multiplyContext.executeOperation(20, 3)); // 输出:20 * 3 = 60 } }
代码解释:
OperationStrategy
是策略接口,定义了所有计算操作的共同行为。AddOperation
、SubtractOperation
、MultiplyOperation
是具体策略,分别实现了加、减、乘的具体算法。CalculatorContext
是上下文类,它持有一个OperationStrategy
对象的引用。客户端通过CalculatorContext
来执行操作,而不需要知道具体的策略类是哪个,实现了策略与客户端的解耦。- 客户端可以根据需要,传入不同的策略对象给
CalculatorContext
,从而在运行时改变其行为。
什么是模板方法模式?一般用在什么场景?
想象一下,你有一张做菜的通用食谱。比如“做一道家常菜”:
- 准备食材
- 切菜
- 下锅炒
- 调味出锅
这个流程是固定的,但具体到“准备什么食材”、“怎么切”、“炒什么菜”、“放什么调料”,每个人家可以有不同的做法。比如,做番茄炒蛋,准备番茄鸡蛋;做青椒肉丝,准备青椒肉。
模板方法模式就是这个意思:在一个抽象类中定义一个操作中的算法骨架(模板),而将一些步骤延迟到子类中去实现。这使得子类可以在不改变算法结构的情况下,重新定义该算法的某些特定步骤。
它解决的问题是:当多个类有共同的算法骨架,但其中某些步骤的实现方式不同时。
一般用在哪些场景?
通用算法框架:
- 比如一个框架,定义了数据处理的通用流程:
读取数据 -> 预处理 -> 核心业务逻辑 -> 保存结果
。 - “读取数据”和“保存结果”可能是固定的(比如都从数据库读写),但“预处理”和“核心业务逻辑”可以由不同的子类来实现(比如一个子类做数据清洗,另一个做数据加密;一个子类是报表生成,另一个是数据分析)。
- 比如一个框架,定义了数据处理的通用流程:
报表生成:
- 生成报表通常有固定步骤:
连接数据库 -> 查询数据 -> 格式化数据 -> 输出报表
。 - 但不同的报表(比如销售报表、财务报表)在“查询数据”和“格式化数据”上会有差异,这些差异的部分就可以由子类去实现。
- 生成报表通常有固定步骤:
算法骨架,具体实现交给子类:
- 比如一个游戏,定义了“玩游戏”的通用步骤:
开始游戏 -> 进行中 -> 结束游戏
。 - 但“进行中”的具体玩法(比如玩象棋、玩扑克)可以由不同的子类来实现。
- 比如一个游戏,定义了“玩游戏”的通用步骤:
Web 开发中的请求处理:
- 比如一个 Spring MVC 控制器,它的请求处理流程可以看作是模板方法:
接收请求 -> 参数绑定 -> 业务逻辑处理 -> 视图渲染
。 - 其中“业务逻辑处理”就是留给开发者实现的“具体步骤”。
- 比如一个 Spring MVC 控制器,它的请求处理流程可以看作是模板方法:
构建工具的编译/打包流程:
- 比如 Maven 或 Gradle,它们定义了构建项目的标准生命周期(
validate -> compile -> test -> package -> install -> deploy
)。 - 这些阶段是固定的,但每个阶段的具体执行(比如用什么编译器编译、如何打包)可以有不同的插件(子类)来实现。
- 比如 Maven 或 Gradle,它们定义了构建项目的标准生命周期(
核心思想:封装“不变”的部分(算法骨架),把“可变”的部分(特定步骤的实现)留给子类。这样,当你需要改变某个步骤的实现时,只需要修改或创建新的子类,而不需要动到算法的整体结构。这也符合“开闭原则”。
示例代码:
我们以“构建房子”为例,房子建造有固定的步骤,但不同类型的房子(木屋、砖房)在某些步骤的实现上有所不同。
抽象模板类(Abstract Template Class)
定义算法的骨架(模板方法),并声明一些抽象方法,供子类实现。也可以包含一些具体方法和钩子方法。
java// Step 1: 定义抽象模板类 public abstract class HouseBuilder { // 模板方法:定义了建造房子的通用算法骨架 public final void buildHouse() { // 使用 final 关键字防止子类修改模板方法的执行顺序 layFoundation(); // 步骤 1: 打地基 (具体方法) buildWalls(); // 步骤 2: 建造墙壁 (抽象方法,子类实现) buildRoof(); // 步骤 3: 建造屋顶 (抽象方法,子类实现) decorateHouse(); // 步骤 4: 装修房子 (钩子方法,子类可选实现) System.out.println("房子建造完成!"); System.out.println("--------------------"); } // 具体方法:所有房子都一样,无需子类修改 private void layFoundation() { System.out.println("打地基:挖掘地基,浇筑混凝土。"); } // 抽象方法:留给子类实现,不同类型的房子墙壁建造方式不同 protected abstract void buildWalls(); // 抽象方法:留给子类实现,不同类型的房子屋顶建造方式不同 protected abstract void buildRoof(); // 钩子方法:子类可以选择是否实现(重写),提供默认的空实现或通用实现 // 比如:不是所有房子都需要装修,或者默认是简单装修 protected void decorateHouse() { System.out.println("装修房子:进行基础装修。"); } }
具体模板实现类(Concrete Template Classes)
实现抽象类中定义的抽象方法,完成算法的具体步骤。
java// Step 2: 实现具体的模板子类 - 木屋 public class WoodenHouseBuilder extends HouseBuilder { @Override protected void buildWalls() { System.out.println("建造墙壁:使用木头和木板搭建墙壁。"); } @Override protected void buildRoof() { System.out.println("建造屋顶:铺设木质瓦片屋顶。"); } // 可以选择性地重写钩子方法,进行特定的装修 @Override protected void decorateHouse() { System.out.println("装修房子:进行木质风格的内外装修。"); } } // Step 2: 实现具体的模板子类 - 砖房 public class BrickHouseBuilder extends HouseBuilder { @Override protected void buildWalls() { System.out.println("建造墙壁:使用砖块和水泥砌墙。"); } @Override protected void buildRoof() { System.out.println("建造屋顶:铺设陶瓷瓦片屋顶。"); } // 这个砖房就不重写 decorateHouse,使用父类的默认装修方法 }
客户端(Client)
使用具体模板实现类来执行模板方法。
java// Step 3: 客户端代码 public class TemplateMethodPatternDemo { public static void main(String[] args) { System.out.println("--- 建造木屋 ---"); HouseBuilder woodenHouse = new WoodenHouseBuilder(); woodenHouse.buildHouse(); // 调用模板方法 System.out.println("\n--- 建造砖房 ---"); HouseBuilder brickHouse = new BrickHouseBuilder(); brickHouse.buildHouse(); // 调用模板方法 } }
代码解释:
HouseBuilder
是抽象模板类,它定义了buildHouse()
这个模板方法。这个方法包含了建造房子的固定步骤顺序。layFoundation()
是一个具体方法,所有房子打地基的方式都一样,所以在父类中直接实现。buildWalls()
和buildRoof()
是抽象方法,它们的具体实现留给子类WoodenHouseBuilder
和BrickHouseBuilder
去完成。decorateHouse()
是一个钩子方法。它在父类中有一个默认的(可能是空的或通用的)实现,子类可以选择性地重写它来添加自己的特定行为。buildHouse()
方法被声明为final
,这是为了防止子类修改算法的骨架,保证了算法的稳定性和一致性。- 客户端通过创建具体子类的实例,然后调用它们的模板方法
buildHouse()
来启动整个建造流程。
谈谈你了解的最常见的几种设计模式,说说他们的应用场景
设计模式就像是前辈们在编程路上遇到一些常见问题时总结出来的“解决方案模板”,不是死板的代码,而是一种解决问题的思路。最常见的几种:
单例模式 (Singleton Pattern)
- 理解方式:全局只有一份,独一无二。就像公司的 CEO,只能有一个。
- 应用场景:
- 日志记录器:整个应用只需要一个日志记录器来处理所有日志,避免多个实例导致日志混乱。
- 配置管理器:应用程序的配置信息通常全局一份,所有模块都从这里获取。
- 数据库连接池:为了节省资源和提高效率,通常只创建一个数据库连接池,供所有需要数据库操作的地方使用。
工厂模式 (Factory Pattern)
- 理解方式:我想买个手机,但我不用知道具体是富士康生产还是比亚迪生产,我只告诉商店我需要一个手机,它就给我一个。
- 应用场景:
- 创建不同类型的图形对象:比如画图软件,用户选择画“圆形”、“方形”或“三角形”,你不需要写一堆
if/else
来判断创建哪个,而是通过一个工厂方法来创建,它会根据你的选择返回对应的图形对象。 - 多数据库支持:你的应用可能需要支持 MySQL、Oracle 或 SQL Server。你可以有一个数据库连接工厂,根据配置返回对应的数据库连接对象,而调用者无需关心是哪种数据库。
- 创建不同类型的图形对象:比如画图软件,用户选择画“圆形”、“方形”或“三角形”,你不需要写一堆
观察者模式 (Observer Pattern)
- 理解方式:就像订阅报纸。报社(被观察者)一出新报纸,所有订阅者(观察者)都会收到通知。
- 应用场景:
- 事件处理:比如用户点击按钮,按钮(被观察者)会通知所有监听这个点击事件的函数(观察者)去执行相应的操作。
- 股票行情系统:股票价格(被观察者)一变动,所有订阅了这只股票的用户(观察者)的客户端都会收到更新。
- MVC 架构:Model 层数据变化,通知 View 层和 Controller 层进行更新。
策略模式 (Strategy Pattern)
- 理解方式:解决一件事有好几种方法,我把这些方法都封装起来,然后根据情况选择其中一个来执行。就像你出门,可以开车、坐地铁、骑自行车,每种都是一种“策略”。
- 应用场景:
- 不同支付方式:在线购物时,可以选择支付宝支付、微信支付、银行卡支付。每种支付方式都是一个策略,根据用户的选择来执行对应的支付逻辑。
- 排序算法:对数据进行排序,可以选择冒泡排序、快速排序、归并排序。根据数据量或特性选择不同的排序算法。
- 计算打折:商场促销有满减、打八折、送优惠券等多种促销策略,根据活动的具体规则选择不同的计算方式。
你认为好的代码应该是什么样的?
好的代码就像一本好书,它有以下几个特点:
易读性 (Readable):
- 像读故事一样:你能快速理解这段代码在做什么,而不是猜谜语。变量名、函数名清晰直观,就像“用户名”而不是“u”、“计算总价”而不是“cal”。
- 格式整齐:缩进规范,空行合理,就像文章分段清晰。
可维护性 (Maintainable):
- 改动不“牵一发而动全身”:修改一个功能时,不会导致其他不相关的功能崩溃或需要大量修改。就像换个车灯,不用把整个发动机都拆下来。
- 容易找到问题:出错了能快速定位是哪一部分代码有问题。
可扩展性 (Extensible):
- 未来可期:当有新需求或新功能要加入时,能轻松地添加,而不是推翻重来。就像盖房子,留好了接口,以后可以轻松加盖一层或加个阳台。
- 对修改关闭,对扩展开放 (OCP 原则):尽量不要修改已有的稳定代码,而是通过新增代码来扩展功能。
健壮性 (Robust):
- 不怕“折腾”:能处理各种异常情况和错误输入,不会轻易崩溃。就像一个好的士兵,在各种恶劣环境下都能完成任务。
- 错误处理完善:及时给出错误提示,或者能优雅地从错误中恢复。
高内聚低耦合 (High Cohesion, Low Coupling):
- 高内聚:一个模块只负责一件事,并且把这件事做到极致。就像一个专业团队,每个人只负责自己擅长的领域。
- 低耦合:模块之间依赖关系少,一个模块的改动对其他模块影响小。就像不同部门之间协作,通过接口而不是直接干涉对方内部细节。
符合设计原则:比如遵循单一职责原则(一个类只做一件事)、里氏替换原则(子类可以替换父类且不破坏程序功能)等,这些都是通往好代码的“秘籍”。
总的来说,好的代码是让人舒服的代码,无论是写的人、读的人还是未来维护的人,都能感觉到它的清晰、高效和可靠。
工厂模式和抽象工厂模式有什么区别?
这就像“生产线”的两种不同管理方式:
工厂模式 (Factory Pattern) - 简单工厂或工厂方法
- 通俗理解:假设你是一个披萨店老板。
- 简单工厂:你有一个“点餐员”(就是工厂),顾客来点餐,无论是“海鲜披萨”还是“培根披萨”,点餐员都会给你做好。你只管告诉点餐员要什么,他就会给你一个做好的披萨。这个点餐员能生产“一种类型”的产品(披萨),但是可以生产这种类型的“不同口味/品种”。
- 工厂方法:你有多个分店,每个分店都有自己的“厨师长”(就是工厂方法),这个厨师长专门做一种披萨。比如“纽约分店的厨师长”专门做纽约风味披萨,“芝加哥分店的厨师长”专门做芝加哥风味披萨。每种厨师长都只生产一种“特定风味”的披萨。
- 主要解决:创建“一类产品”的不同“具体实现”。例如,创建不同种类的汽车(轿车、SUV),或者不同种类的文档(PDF、Word)。
- 特点:
- 通常一个工厂(或一个工厂方法)只负责创建一种类型的产品。
- 当需要新增一种产品时,可能需要修改工厂代码(简单工厂)或者新增一个工厂类(工厂方法)。
示例:我们来创建一个披萨工厂,它可以根据用户的需求生产不同类型的披萨。
首先,定义披萨的抽象类或接口:
java// 披萨的抽象基类 abstract class Pizza { protected String name; public abstract void prepare(); public abstract void bake(); public abstract void cut(); public abstract void box(); public String getName() { return name; } } // 具体披萨类型 1:芝士披萨 class CheesePizza extends Pizza { public CheesePizza() { name = "芝士披萨"; } @Override public void prepare() { System.out.println("准备芝士披萨的原料:面团、番茄酱、大量芝士"); } @Override public void bake() { System.out.println("烘烤芝士披萨,200 度 15 分钟"); } @Override public void cut() { System.out.println("将芝士披萨切成 8 块"); } @Override public void box() { System.out.println("将芝士披萨装入披萨盒"); } } // 具体披萨类型 2:培根披萨 class BaconPizza extends Pizza { public BaconPizza() { name = "培根披萨"; } @Override public void prepare() { System.out.println("准备培根披萨的原料:面团、番茄酱、培根、洋葱"); } @Override public void bake() { System.out.println("烘烤培根披萨,200 度 18 分钟"); } @Override public void cut() { System.out.println("将培根披萨切成 6 块"); } @Override public void box() { System.out.println("将培根披萨装入披萨盒"); } }
然后,定义披萨工厂的抽象接口和具体工厂类:
java// 披萨工厂的抽象接口 // 每个具体工厂负责生产一种类型的披萨 interface PizzaFactory { Pizza createPizza(); } // 芝士披萨工厂 class CheesePizzaFactory implements PizzaFactory { @Override public Pizza createPizza() { System.out.println("----通过芝士披萨工厂生产芝士披萨----"); return new CheesePizza(); } } // 培根披萨工厂 class BaconPizzaFactory implements PizzaFactory { @Override public Pizza createPizza() { System.out.println("----通过培根披萨工厂生产培根披萨----"); return new BaconPizza(); } }
最后,客户端如何使用:
javapublic class FactoryMethodDemo { public static void main(String[] args) { System.out.println("欢迎来到披萨店!"); // 客户想要一个芝士披萨 PizzaFactory cheeseFactory = new CheesePizzaFactory(); Pizza cheesePizza = cheeseFactory.createPizza(); System.out.println("你点的是:" + cheesePizza.getName()); cheesePizza.prepare(); cheesePizza.bake(); cheesePizza.cut(); cheesePizza.box(); System.out.println(); // 客户想要一个培根披萨 PizzaFactory baconFactory = new BaconPizzaFactory(); Pizza baconPizza = baconFactory.createPizza(); System.out.println("你点的是:" + baconPizza.getName()); baconPizza.prepare(); baconPizza.bake(); baconPizza.cut(); baconPizza.box(); } }
运行结果:
txt欢迎来到披萨店! ----通过芝士披萨工厂生产芝士披萨---- 你点的是:芝士披萨 准备芝士披萨的原料:面团、番茄酱、大量芝士 烘烤芝士披萨,200 度 15 分钟 将芝士披萨切成 8 块 将芝士披萨装入披萨盒 ----通过培根披萨工厂生产培根披萨---- 你点的是:培根披萨 准备培根披萨的原料:面团、番茄酱、培根、洋葱 烘烤培根披萨,200 度 18 分钟 将培根披萨切成 6 块 将培根披萨装入披萨盒
解释:
在这个例子中,
PizzaFactory
是抽象工厂,定义了创建披萨的方法。CheesePizzaFactory
和BaconPizzaFactory
是具体工厂,每个工厂只负责创建一种具体类型的披萨(芝士披萨或培根披萨)。当需要增加一种新的披萨类型(比如“蔬菜披萨”)时,我们只需要创建一个新的VegetablePizza
类和一个新的VegetablePizzaFactory
类,而不需要修改已有的工厂或披萨类。这符合“开闭原则”。
抽象工厂模式 (Abstract Factory Pattern)
- 通俗理解:回到披萨店。现在你的店不只卖披萨了,还卖饮品和甜点。
- 抽象工厂就像是“整个餐厅的运营管理团队”。
- 你有一个“纽约风格餐厅运营团队”,它知道怎么生产“纽约风味披萨”、“纽约风味饮料”和“纽约风味甜点”。
- 你还有一个“芝加哥风格餐厅运营团队”,它知道怎么生产“芝加哥风味披萨”、“芝加哥风味饮料”和“芝加哥风味甜点”。
- 你想要一套“纽约风格”的产品(披萨、饮料、甜点),你只需要告诉“纽约风格餐厅运营团队”,它就会给你生产出来。
- 主要解决:创建“一系列相关或相互依赖的产品族”。例如,你不仅仅需要“芝士披萨”,可能还需要一套“芝士披萨家族”的产品,包括“芝士披萨”、“芝士口味饮料”、“芝士口味甜点”。
- 特点:
- 一个抽象工厂可以生产多个不同类型但属于同一“产品族”的产品。
- 当需要新增一个“产品族”时(例如,新增“意大利风格”的产品系列),需要新增一个具体的抽象工厂实现类。而新增单个产品(例如,在现有产品族里新增一种披萨),可能需要修改所有相关工厂。
示例:披萨店升级了,不只卖披萨,还卖饮料和甜点,而且披萨店有“纽约风味”和“芝加哥风味”两种风格。每种风格的披萨、饮料、甜点都是一套。
首先,定义产品族(披萨、饮料、甜点)的抽象类或接口:
java// --- 产品族:披萨 --- abstract class AbsPizza { protected String style; public abstract void prepare(); public abstract void bake(); public String getStyle() { return style; } } class NYStyleCheesePizza extends AbsPizza { public NYStyleCheesePizza() { style = "纽约风味芝士披萨"; } @Override public void prepare() { System.out.println("准备纽约风味芝士披萨:薄底面团、马苏里拉芝士..."); } @Override public void bake() { System.out.println("烘烤纽约风味芝士披萨..."); } } class ChicagoStyleCheesePizza extends AbsPizza { public ChicagoStyleCheesePizza() { style = "芝加哥风味芝士披萨"; } @Override public void prepare() { System.out.println("准备芝加哥风味芝士披萨:厚底面团、深盘、大量芝士..."); } @Override public void bake() { System.out.println("烘烤芝加哥风味芝士披萨..."); } } // --- 产品族:饮料 --- abstract class AbsDrink { protected String style; public abstract void serve(); public String getStyle() { return style; } } class NYStyleSoda extends AbsDrink { public NYStyleSoda() { style = "纽约风味苏打水"; } @Override public void serve() { System.out.println("提供纽约风味苏打水:加冰、柠檬片..."); } } class ChicagoStyleCola extends AbsDrink { public ChicagoStyleCola() { style = "芝加哥风味可乐"; } @Override public void serve() { System.out.println("提供芝加哥风味可乐:大杯装、少冰..."); } } // --- 产品族:甜点 --- abstract class AbsDessert { protected String style; public abstract void serve(); public String getStyle() { return style; } } class NYStyleCheesecake extends AbsDessert { public NYStyleCheesecake() { style = "纽约风味芝士蛋糕"; } @Override public void serve() { System.out.println("提供纽约风味芝士蛋糕:小巧精致..."); } } class ChicagoStyleBrownie extends AbsDessert { public ChicagoStyleBrownie() { style = "芝加哥风味布朗尼"; } @Override public void serve() { System.out.println("提供芝加哥风味布朗尼:浓郁厚实..."); } }
然后,定义抽象工厂和具体工厂:
java// 抽象工厂接口:定义创建一组产品的方法 interface RestaurantFactory { AbsPizza createPizza(); AbsDrink createDrink(); AbsDessert createDessert(); } // 纽约风味餐厅的具体工厂 class NYStyleRestaurantFactory implements RestaurantFactory { @Override public AbsPizza createPizza() { return new NYStyleCheesePizza(); } @Override public AbsDrink createDrink() { return new NYStyleSoda(); } @Override public AbsDessert createDessert() { return new NYStyleCheesecake(); } } // 芝加哥风味餐厅的具体工厂 class ChicagoStyleRestaurantFactory implements RestaurantFactory { @Override public AbsPizza createPizza() { return new ChicagoStyleCheesePizza(); } @Override public AbsDrink createDrink() { return new ChicagoStyleCola(); } @Override public AbsDessert createDessert() { return new ChicagoStyleBrownie(); } }
最后,客户端如何使用:
javapublic class AbstractFactoryDemo { public static void main(String[] args) { System.out.println("欢迎来到全球披萨店连锁!"); // 客户想点一套纽约风味的餐品 System.out.println("\n--- 顾客点了一套纽约风味餐品 ---"); RestaurantFactory nyFactory = new NYStyleRestaurantFactory(); AbsPizza nyPizza = nyFactory.createPizza(); AbsDrink nyDrink = nyFactory.createDrink(); AbsDessert nyDessert = nyFactory.createDessert(); System.out.println("你的纽约风味套餐包含:"); System.out.println("- " + nyPizza.getStyle()); nyPizza.prepare(); nyPizza.bake(); System.out.println("- " + nyDrink.getStyle()); nyDrink.serve(); System.out.println("- " + nyDessert.getStyle()); nyDessert.serve(); // 客户想点一套芝加哥风味的餐品 System.out.println("\n--- 顾客点了一套芝加哥风味餐品 ---"); RestaurantFactory chicagoFactory = new ChicagoStyleRestaurantFactory(); AbsPizza chicagoPizza = chicagoFactory.createPizza(); AbsDrink chicagoDrink = chicagoFactory.createDrink(); AbsDessert chicagoDessert = chicagoFactory.createDessert(); System.out.println("你的芝加哥风味套餐包含:"); System.out.println("- " + chicagoPizza.getStyle()); chicagoPizza.prepare(); chicagoPizza.bake(); System.out.println("- " + chicagoDrink.getStyle()); chicagoDrink.serve(); System.out.println("- " + chicagoDessert.getStyle()); chicagoDessert.serve(); } }
运行结果:
txt欢迎来到全球披萨店连锁! --- 顾客点了一套纽约风味餐品 --- 你的纽约风味套餐包含: - 纽约风味芝士披萨 准备纽约风味芝士披萨:薄底面团、马苏里拉芝士... 烘烤纽约风味芝士披萨... - 纽约风味苏打水 提供纽约风味苏打水:加冰、柠檬片... - 纽约风味芝士蛋糕 提供纽约风味芝士蛋糕:小巧精致... --- 顾客点了一套芝加哥风味餐品 --- 你的芝加哥风味套餐包含: - 芝加哥风味芝士披萨 准备芝加哥风味芝士披萨:厚底面团、深盘、大量芝士... 烘烤芝加哥风味芝士披萨... - 芝加哥风味可乐 提供芝加哥风味可乐:大杯装、少冰... - 芝加哥风味布朗尼 提供芝加哥风味布朗尼:浓郁厚实...
抽象工厂模式解释:
在这个例子中,
RestaurantFactory
是抽象工厂,定义了创建披萨、饮料和甜点的方法。NYStyleRestaurantFactory
和ChicagoStyleRestaurantFactory
是具体工厂,每个工厂负责创建一整套特定风格的产品(纽约风味的披萨、饮料、甜点;或芝加哥风味的披萨、饮料、甜点)。如果我们要新增一种“意大利风味”的餐厅,我们只需要创建一个新的
ItalianStyleRestaurantFactory
类,实现RestaurantFactory
接口,并让它创建对应的意大利风味披萨、饮料和甜点即可。客户端代码只需要切换具体的工厂实例,就能获得不同风格的整套产品,而无需关心内部创建的细节。
核心区别总结:
- 工厂模式:生产一种产品类型的不同具体实现。
- 目标:创建一个产品。
- 例子:我想要一个“车”,工厂给我一个“轿车”或“SUV”。
- 抽象工厂模式:生产一系列相关或相互依赖的产品家族。
- 目标:创建一套产品。
- 例子:我想要一套“德国品牌”的“车、电视、洗衣机”,抽象工厂给我“奔驰轿车、西门子电视、博世洗衣机”。
简单来说,工厂模式关注的是“造一个什么东西”,而抽象工厂模式关注的是“造一套什么风格的东西”。