设计模式3-行为型模式
设计模式3-行为型模式
作用:通过规范对象间的交互方式和职责分配,解决对象协作中的行为协调问题,使对象通信更灵活、职责更清晰。
1 访问者模式(Visitor Pattern)教程
一、什么是访问者模式?
访问者模式是 ** 行为型模式中专注于 “分离数据结构与操作”** 的核心模式,其核心思想是:在不修改集合中元素类的前提下,通过定义 “访问者” 对象,为集合中的不同类型元素添加新的操作行为。
简单说:“数据结构(如文件、商品)固定不变,但对数据的操作(如统计、格式化)经常变化时,用访问者封装操作,避免修改数据类本身”。
日常生活中,超市收银员扫描不同商品(数据元素)并计算总价(访问者操作)、审计人员检查不同类型的凭证(数据元素)并记录问题(访问者操作),都是访问者模式的体现 —— 商品 / 凭证的类型(数据结构)相对稳定,但计算、审计等操作(访问者)可灵活新增。
二、为什么需要访问者模式?(作用)
当系统中存在稳定的数据结构(如多种类型的元素集合)和易变的操作行为(如对元素的统计、分析、格式化) 时,直接在元素类中添加操作会导致:
- 元素类频繁修改,违反 “开闭原则”(新增操作需修改所有元素类);
- 操作逻辑分散在各个元素类中,难以维护和复用(同类操作分散在不同类);
- 数据结构与操作强耦合,元素类承担过多职责,违背 “单一职责原则”。
访问者模式的核心作用是:
- 分离数据结构与操作:数据元素类只负责存储数据,操作逻辑封装在访问者中,两者独立变化;
- 支持灵活新增操作:新增操作只需新增访问者类,无需修改元素类和数据结构,符合 “开闭原则”;
- 集中管理同类操作:将对不同元素的同一类操作(如统计、导出)集中在一个访问者中,便于维护和复用;
- 处理多类型元素集合:访问者可根据元素的具体类型执行不同操作,适配集合中多种元素的差异化处理需求。
三、反例:操作与数据结构耦合的问题
假设我们要设计一个文件管理系统,包含文本文件(TextFile)和图片文件(ImageFile),需要实现 “统计文件大小” 和 “格式化文件名称” 两种操作。
不使用访问者模式的实现:
// 1. 文本文件类(包含数据和操作)
class TextFile {
private String name;
private long size; // 单位:字节
public TextFile(String name, long size) {
this.name = name;
this.size = size;
}
// 操作1:统计大小
public long calculateSize() {
return size;
}
// 操作2:格式化名称
public String formatName() {
return "[文本文件] " + name;
}
// getter
public String getName() { return name; }
public long getSize() { return size; }
}
// 2. 图片文件类(包含数据和操作)
class ImageFile {
private String name;
private long size; // 单位:字节
private String resolution; // 分辨率(图片特有属性)
public ImageFile(String name, long size, String resolution) {
this.name = name;
this.size = size;
this.resolution = resolution;
}
// 操作1:统计大小(与文本文件逻辑相同,代码重复)
public long calculateSize() {
return size;
}
// 操作2:格式化名称(与文本文件逻辑不同)
public String formatName() {
return "[图片文件] " + name + "(" + resolution + ")";
}
// getter
public String getName() { return name; }
public long getSize() { return size; }
public String getResolution() { return resolution; }
}
// 客户端:直接调用元素的操作方法(问题核心)
public class Client {
public static void main(String[] args) {
List<Object> files = new ArrayList<>();
files.add(new TextFile("笔记.txt", 1024));
files.add(new ImageFile("照片.jpg", 2048, "1920x1080"));
// 操作1:统计总大小
long totalSize = 0;
for (Object file : files) {
// 需判断元素类型,调用对应方法(繁琐且易出错)
if (file instanceof TextFile) {
totalSize += ((TextFile) file).calculateSize();
} else if (file instanceof ImageFile) {
totalSize += ((ImageFile) file).calculateSize();
}
}
System.out.println("总大小:" + totalSize + "字节");
// 操作2:格式化名称
for (Object file : files) {
if (file instanceof TextFile) {
System.out.println(((TextFile) file).formatName());
} else if (file instanceof ImageFile) {
System.out.println(((ImageFile) file).formatName());
}
}
// 问题:新增操作(如导出文件信息)需修改所有文件类,且客户端需新增遍历判断逻辑
}
}
问题分析:
- 违反开闭原则:新增操作(如 “导出文件信息”)时,需在
TextFile和ImageFile中都新增export()方法,同时客户端需新增遍历判断逻辑,修改成本高; - 代码冗余:同类操作的重复逻辑(如
calculateSize()在两个文件类中逻辑相同)无法复用,维护困难; - 客户端逻辑繁琐:客户端需手动判断元素类型并调用对应方法,若元素类型增多(如新增 “视频文件”),判断逻辑会急剧膨胀;
- 数据与操作耦合:文件类既存储数据(如
name、size),又承担操作逻辑(如formatName),违背 “单一职责原则”。
四、正例:用访问者模式解决问题
核心改进:分离 “数据元素” 和 “操作行为”,定义 “访问者” 接口封装操作,元素类提供 “接受访问者” 的方法,客户端通过访问者统一处理不同元素的操作,无需关心元素类型。
访问者模式的实现:
import java.util.ArrayList;
import java.util.List;
// 1. 抽象访问者(Visitor):定义对所有元素的操作接口
interface FileVisitor {
// 对文本文件的操作
void visit(TextFile textFile);
// 对图片文件的操作
void visit(ImageFile imageFile);
}
// 2. 具体访问者1:实现“统计大小”操作
class SizeCalculateVisitor implements FileVisitor {
private long totalSize = 0;
@Override
public void visit(TextFile textFile) {
// 处理文本文件的大小统计
totalSize += textFile.getSize();
System.out.println("统计文本文件:" + textFile.getName() + ",大小:" + textFile.getSize() + "字节");
}
@Override
public void visit(ImageFile imageFile) {
// 处理图片文件的大小统计
totalSize += imageFile.getSize();
System.out.println("统计图片文件:" + imageFile.getName() + ",大小:" + imageFile.getSize() + "字节");
}
// 获取总大小
public long getTotalSize() {
return totalSize;
}
}
// 3. 具体访问者2:实现“格式化名称”操作
class NameFormatVisitor implements FileVisitor {
@Override
public void visit(TextFile textFile) {
// 文本文件名称格式化
String formattedName = "[文本文件] " + textFile.getName();
System.out.println("格式化名称:" + formattedName);
}
@Override
public void visit(ImageFile imageFile) {
// 图片文件名称格式化(包含分辨率)
String formattedName = "[图片文件] " + imageFile.getName() + "(" + imageFile.getResolution() + ")";
System.out.println("格式化名称:" + formattedName);
}
}
// 4. 抽象元素(Element):定义接受访问者的方法(核心)
interface FileElement {
// 接受访问者,将自身传递给访问者
void accept(FileVisitor visitor);
}
// 5. 具体元素1:文本文件(实现接受访问者的方法)
class TextFile implements FileElement {
private String name;
private long size;
public TextFile(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public void accept(FileVisitor visitor) {
// 调用访问者对文本文件的操作
visitor.visit(this);
}
// getter
public String getName() { return name; }
public long getSize() { return size; }
}
// 6. 具体元素2:图片文件(实现接受访问者的方法)
class ImageFile implements FileElement {
private String name;
private long size;
private String resolution;
public ImageFile(String name, long size, String resolution) {
this.name = name;
this.size = size;
this.resolution = resolution;
}
@Override
public void accept(FileVisitor visitor) {
// 调用访问者对图片文件的操作
visitor.visit(this);
}
// getter
public String getName() { return name; }
public long getSize() { return size; }
public String getResolution() { return resolution; }
}
// 7. 对象结构(Object Structure):管理元素集合,提供遍历接口
class FileStructure {
private List<FileElement> files = new ArrayList<>();
// 添加元素
public void addFile(FileElement file) {
files.add(file);
}
// 让访问者遍历所有元素
public void accept(FileVisitor visitor) {
for (FileElement file : files) {
file.accept(visitor);
}
}
}
// 8. 客户端:通过访问者和对象结构操作元素,无需关心元素类型
public class Client {
public static void main(String[] args) {
// 1. 创建元素
FileElement textFile = new TextFile("笔记.txt", 1024);
FileElement imageFile = new ImageFile("照片.jpg", 2048, "1920x1080");
// 2. 创建对象结构,管理元素
FileStructure fileStructure = new FileStructure();
fileStructure.addFile(textFile);
fileStructure.addFile(imageFile);
// 3. 操作1:统计大小(使用大小访问者)
SizeCalculateVisitor sizeVisitor = new SizeCalculateVisitor();
fileStructure.accept(sizeVisitor);
System.out.println("所有文件总大小:" + sizeVisitor.getTotalSize() + "字节\n");
// 4. 操作2:格式化名称(使用名称访问者)
NameFormatVisitor formatVisitor = new NameFormatVisitor();
fileStructure.accept(formatVisitor);
// 5. 新增操作(如导出文件信息):只需新增访问者,无需修改元素类和对象结构
// FileVisitor exportVisitor = new ExportVisitor();
// fileStructure.accept(exportVisitor);
}
}
改进效果:
- 符合开闭原则:新增操作(如 “导出文件信息”)时,只需新增
ExportVisitor实现FileVisitor接口,无需修改TextFile、ImageFile等元素类,也无需修改对象结构,扩展成本极低; - 分离数据与操作:元素类(
TextFile、ImageFile)仅负责存储数据和提供accept方法,操作逻辑集中在访问者中,职责清晰,符合 “单一职责原则”; - 客户端逻辑简化:客户端无需判断元素类型,只需创建对应访问者并通过对象结构执行,代码简洁且不易出错;
- 操作复用与集中管理:同类操作(如统计、格式化)集中在一个访问者中,避免代码冗余,修改时只需调整访问者类;
- 支持多类型元素:新增元素类型(如 “视频文件
VideoFile”)时,只需在FileVisitor中新增visit(VideoFile videoFile)方法,并让VideoFile实现FileElement,现有访问者可选择性扩展,灵活性高。
五、访问者模式的核心结构
访问者模式通过 “访问者主动访问元素” 实现操作与数据的分离,包含 5 个核心角色:
- 抽象访问者(Visitor):
- 定义对所有具体元素的操作接口(如
FileVisitor的visit(TextFile)、visit(ImageFile)); - 每个方法对应一种元素类型,方法参数为具体元素实例,确保访问者能获取元素的详细信息并执行操作。
- 定义对所有具体元素的操作接口(如
- 具体访问者(Concrete Visitor):
- 实现
Visitor接口,完成对具体元素的操作逻辑(如SizeCalculateVisitor的大小统计、NameFormatVisitor的名称格式化); - 可维护操作过程中的状态(如
SizeCalculateVisitor的totalSize),支持复杂操作的上下文管理。
- 实现
- 抽象元素(Element):
- 定义元素的核心接口,关键是声明 “接受访问者” 的方法(如
FileElement的accept(FileVisitor visitor)); - 该方法将元素自身传递给访问者,触发访问者对该元素的具体操作,是访问者与元素交互的桥梁。
- 定义元素的核心接口,关键是声明 “接受访问者” 的方法(如
- 具体元素(Concrete Element):
- 实现
Element接口,重写accept方法,在方法内部调用访问者对应类型的visit方法(如TextFile的accept调用visitor.visit(this)); - 存储元素的具体数据(如
name、size),并提供 getter 方法供访问者获取数据。
- 实现
- 对象结构(Object Structure):
- 管理元素的集合(如
FileStructure的files列表),提供添加、删除元素的方法; - 提供
accept方法,遍历集合中的所有元素,让每个元素接受访问者的访问,简化客户端对元素集合的操作。
- 管理元素的集合(如
六、总结
访问者模式的核心是 “分离数据结构与操作行为”,通过访问者封装易变的操作,让数据元素类保持稳定,从而在不修改元素的前提下灵活扩展功能。
它的优势在于操作扩展的灵活性和集中管理,适合数据结构稳定但操作多变的场景;但缺点是元素类型扩展困难,且会提升系统复杂度。实际开发中,需权衡 “操作扩展” 和 “元素扩展” 的需求 —— 若操作频繁新增而元素类型稳定,访问者模式是最优解;若元素类型频繁新增,则不适合使用。
记住:访问者模式是 “操作扩展” 的利器,让数据结构 “不变”,让操作行为 “多变”。
2 策略模式(Strategy Pattern)
一、什么是策略模式?
策略模式是 ** 行为型模式中专注于 “算法灵活切换与复用”** 的核心模式,其核心思想是:定义一系列可互换的算法(策略),将每个算法封装成独立的类,使算法的选择与使用分离,客户端可根据需求动态切换不同的算法,而无需修改使用算法的代码。
简单说:“像手机支付时选择‘支付宝’或‘微信支付’一样,不同支付方式(策略)实现相同的支付功能,用户可按需切换,支付流程(使用策略的代码)无需改变”。
日常生活中,导航软件的 “路线规划”(步行、公交、驾车)、电商平台的 “折扣计算”(满减、打折、优惠券)都是策略模式的体现 —— 核心功能(导航、计算价格)不变,但具体实现(不同路线、不同折扣)可灵活替换。
二、为什么需要策略模式?(作用)
当系统中存在多种类似的算法(或行为),且需要根据不同场景动态选择时,直接在代码中用if-else或switch-case硬编码会导致:
- 代码臃肿,新增算法需修改原有逻辑(违反 “开闭原则”);
- 算法与使用算法的代码强耦合,难以单独复用或测试;
- 可读性差,大量条件判断掩盖核心业务逻辑。
策略模式的核心作用是:
- 消除条件判断:用 “多态” 替代
if-else,算法的选择由客户端动态决定,代码更简洁; - 支持动态切换:运行时可根据场景切换算法(如导航时从 “驾车” 切换为 “步行”),灵活性高;
- 算法独立复用:每个算法封装在独立类中,可在不同场景单独复用(如 “满减折扣” 同时用于购物车和促销活动);
- 符合开闭原则:新增算法只需新增策略类,无需修改使用算法的代码,扩展成本低。
三、反例:用条件判断实现多算法的问题
假设我们要设计一个电商购物车的 “折扣计算” 功能,支持三种折扣方式:无折扣、满 300 减 50、打 9 折。
不使用策略模式的实现:
// 购物车类(包含折扣计算逻辑,问题核心)
class ShoppingCart {
private double totalPrice; // 总价
private String discountType; // 折扣类型:"NONE"、"FULL300MINUS50"、"NINETY_PERCENT"
public ShoppingCart(double totalPrice, String discountType) {
this.totalPrice = totalPrice;
this.discountType = discountType;
}
// 计算折后价(用if-else硬编码所有算法)
public double calculateFinalPrice() {
if ("NONE".equals(discountType)) {
return totalPrice; // 无折扣
} else if ("FULL300MINUS50".equals(discountType)) {
return totalPrice >= 300 ? totalPrice - 50 : totalPrice; // 满300减50
} else if ("NINETY_PERCENT".equals(discountType)) {
return totalPrice * 0.9; // 9折
} else {
throw new IllegalArgumentException("未知折扣类型");
}
}
}
// 客户端:使用购物车
public class Client {
public static void main(String[] args) {
// 场景1:无折扣
ShoppingCart cart1 = new ShoppingCart(200, "NONE");
System.out.println("无折扣价:" + cart1.calculateFinalPrice()); // 200.0
// 场景2:满300减50
ShoppingCart cart2 = new ShoppingCart(400, "FULL300MINUS50");
System.out.println("满减后价:" + cart2.calculateFinalPrice()); // 350.0
// 场景3:9折
ShoppingCart cart3 = new ShoppingCart(200, "NINETY_PERCENT");
System.out.println("9折价:" + cart3.calculateFinalPrice()); // 180.0
// 问题:新增折扣(如“满500减100”)需修改ShoppingCart的calculateFinalPrice方法,违反开闭原则
// 问题:折扣逻辑与购物车强耦合,无法在其他场景(如促销活动)复用“满300减50”的计算逻辑
// 问题:条件判断过多,后续维护成本高(如修改“满300减50”为“满300减60”需找到对应if分支)
}
}
问题分析:
- 违反开闭原则:新增折扣方式(如 “满 500 减 100”)时,必须修改
ShoppingCart的calculateFinalPrice方法,添加新的if分支,原有代码面临被破坏的风险; - 耦合度高:折扣算法与购物车类强绑定,若其他模块(如订单确认页)也需要计算折扣,需重复编写相同逻辑,无法复用;
- 可读性差:当折扣方式增多(如 10 种),
calculateFinalPrice方法会充斥大量if-else,核心逻辑被条件判断掩盖,难以理解和调试; - 测试困难:测试某一种折扣算法时,需初始化整个购物车,且无法单独测试算法本身(算法与购物车逻辑混合)。
四、正例:用策略模式解决问题
核心改进:将每种折扣算法封装成独立的 “策略类”,定义统一的策略接口,购物车(上下文)通过策略接口调用算法,客户端可动态设置策略(折扣方式)。
策略模式的实现:
// 1. 抽象策略(Strategy):定义折扣算法的统一接口
interface DiscountStrategy {
double calculate(double totalPrice); // 计算折后价
}
// 2. 具体策略1:无折扣
class NoneDiscount implements DiscountStrategy {
@Override
public double calculate(double totalPrice) {
return totalPrice;
}
}
// 3. 具体策略2:满300减50
class Full300Minus50Discount implements DiscountStrategy {
@Override
public double calculate(double totalPrice) {
return totalPrice >= 300 ? totalPrice - 50 : totalPrice;
}
}
// 4. 具体策略3:9折
class NinetyPercentDiscount implements DiscountStrategy {
@Override
public double calculate(double totalPrice) {
return totalPrice * 0.9;
}
}
// 5. 上下文(Context):使用策略的类,持有策略接口引用
class ShoppingCart {
private double totalPrice;
// 持有策略接口(不依赖具体策略,依赖抽象)
private DiscountStrategy discountStrategy;
// 构造函数:初始化总价和默认策略(无折扣)
public ShoppingCart(double totalPrice) {
this.totalPrice = totalPrice;
this.discountStrategy = new NoneDiscount(); // 默认无折扣
}
// 动态设置策略(允许客户端切换折扣方式)
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
// 计算折后价:委托给当前策略
public double calculateFinalPrice() {
return discountStrategy.calculate(totalPrice);
}
}
// 6. 客户端:选择并设置策略,无需关心算法细节
public class Client {
public static void main(String[] args) {
// 创建购物车(总价400)
ShoppingCart cart = new ShoppingCart(400);
// 场景1:使用无折扣策略
cart.setDiscountStrategy(new NoneDiscount());
System.out.println("无折扣价:" + cart.calculateFinalPrice()); // 400.0
// 场景2:切换为满300减50策略
cart.setDiscountStrategy(new Full300Minus50Discount());
System.out.println("满减后价:" + cart.calculateFinalPrice()); // 350.0
// 场景3:切换为9折策略
cart.setDiscountStrategy(new NinetyPercentDiscount());
System.out.println("9折价:" + cart.calculateFinalPrice()); // 360.0
// 新增策略:满500减100(无需修改原有类,符合开闭原则)
cart.setDiscountStrategy(new Full500Minus100Discount());
System.out.println("满500减100价:" + cart.calculateFinalPrice()); // 400.0(不满500,不减免)
}
// 新增具体策略:满500减100(仅需新增类,无需修改其他代码)
static class Full500Minus100Discount implements DiscountStrategy {
@Override
public double calculate(double totalPrice) {
return totalPrice >= 500 ? totalPrice - 100 : totalPrice;
}
}
}
改进效果:
- 符合开闭原则:新增折扣方式(如 “满 500 减 100”)时,只需新增
Full500Minus100Discount实现DiscountStrategy,ShoppingCart和其他策略类无需修改,扩展安全且成本低; - 消除条件判断:
ShoppingCart的calculateFinalPrice方法通过策略接口调用算法,无需if-else,代码简洁清晰,核心逻辑突出; - 算法独立复用:每个折扣策略(如
Full300Minus50Discount)是独立类,可在其他模块(如订单系统、促销活动)直接复用,避免代码重复; - 动态切换策略:客户端可通过
setDiscountStrategy在运行时切换算法(如用户下单时临时选择优惠券),灵活性远高于硬编码; - 便于测试:可单独测试每个策略类(如测试
NinetyPercentDiscount时,只需传入总价参数,无需依赖购物车),测试效率更高。
五、策略模式的核心结构
策略模式通过 “接口定义 + 多态实现” 实现算法的灵活切换,包含 3 个核心角色:
- 抽象策略(Strategy):
- 定义所有具体策略的统一接口(如
DiscountStrategy的calculate方法); - 声明算法的核心方法,是上下文与具体策略交互的桥梁,保证策略的可替换性。
- 定义所有具体策略的统一接口(如
- 具体策略(Concrete Strategy):
- 实现
Strategy接口,封装具体的算法逻辑(如Full300Minus50Discount的满减计算); - 多个具体策略实现不同的算法,但遵循相同的接口,确保可相互替换。
- 实现
- 上下文(Context):
- 是使用策略的角色(如
ShoppingCart),持有Strategy接口的引用; - 提供接口供客户端设置具体策略(如
setDiscountStrategy); - 自身不实现算法,而是将算法的调用委托给当前持有的策略对象(如
calculateFinalPrice调用discountStrategy.calculate)。
- 是使用策略的角色(如
六、策略模式的工作原理
策略模式的核心是 “接口隔离 + 委托执行”:
- 接口隔离:抽象策略接口隔离了上下文与具体策略的直接依赖(上下文只依赖
Strategy接口,不依赖具体策略类),确保策略可替换; - 委托执行:上下文将算法的执行委托给当前设置的具体策略对象,自身不参与算法逻辑,实现 “使用” 与 “实现” 的分离。
这种机制保证了:
- 算法的新增 / 修改不影响上下文和其他策略;
- 客户端可根据需求动态选择策略,无需修改上下文代码;
- 每个策略专注于自身算法,符合 “单一职责原则”。
七、策略模式的优缺点
优点:
- 消除条件判断:用多态替代
if-else/switch-case,代码更简洁,可读性更高; - 符合开闭原则:新增策略只需新增类,无需修改原有代码,扩展灵活;
- 算法独立复用:每个策略是独立类,可在不同场景复用,避免代码冗余;
- 便于测试:策略类可单独测试,无需依赖上下文,测试效率高;
- 动态切换算法:运行时可通过
set方法切换策略,适应场景变化(如导航路线实时切换)。
缺点:
- 客户端需了解所有策略:客户端必须知道存在哪些策略才能选择,增加了客户端的认知成本(可通过工厂模式解决);
- 策略类数量增多:每种算法对应一个策略类,策略过多时会导致类数量膨胀(可通过结合享元模式复用策略对象);
- 策略与上下文可能存在数据依赖:若策略需要上下文的大量数据,可能导致策略与上下文耦合度升高(需合理设计策略接口参数)。
八、适用场景
策略模式适用于 “存在多种类似算法,且需要动态选择或切换” 的场景:
- 多种算法完成同一任务:如排序算法(冒泡、快排、归并)、加密算法(MD5、SHA-256)、支付方式(支付宝、微信、银联);
- 避免条件判断臃肿:当代码中出现超过 3 个相关的
if-else或switch-case,且判断逻辑是 “选择不同算法” 时,适合用策略模式重构; - 算法需要动态切换:如导航软件根据实时路况切换路线算法(拥堵时切换为 “躲避拥堵” 策略);
- 算法需要复用:同一算法需在多个模块中使用(如 “满减折扣” 同时用于购物车、限时活动、会员体系)。
典型案例:
- Java 中的
Comparator接口(不同比较策略实现对象排序); - Spring 的
Resource接口(不同资源加载策略:文件、URL、类路径); - 日志框架的日志级别策略(不同级别对应不同的日志输出策略)。
九、策略模式 vs 状态模式
两者结构相似(都通过接口和多态实现行为变化),但核心目标和行为驱动方式不同:
| 对比维度 | 策略模式 | 状态模式 |
|---|---|---|
| 核心目标 | 封装可互换的算法,客户端主动选择策略 | 封装对象的状态及状态对应的行为,状态由对象内部变化驱动 |
| 行为触发方式 | 客户端主动设置策略(外部驱动) | 状态自动切换(内部状态变化驱动) |
| 策略 / 状态关系 | 策略之间无依赖,完全独立 | 状态之间可能存在转换关系(如 “订单待支付→已支付→已发货”) |
| 适用场景 | 算法选择(支付方式、排序) | 状态驱动的行为(订单状态、生命周期) |
十、总结
策略模式的核心是 “算法封装,动态切换”,通过将算法与使用算法的代码分离,解决了条件判断臃肿和扩展困难的问题,使系统更灵活、易维护。
它的关键是抽象策略接口的设计(确保策略可替换)和上下文对策略的委托执行(实现使用与实现的分离)。实际开发中,当遇到 “多种算法需要灵活选择” 的场景时,策略模式是消除if-else、提升代码质量的最佳实践。
记住:策略模式让算法的选择 “随心所欲”,让代码的扩展 “轻而易举”。
3 备忘录模式(Memento Pattern)
一、什么是备忘录模式?
备忘录模式是 ** 行为型模式中专注于 “对象状态的保存与恢复”** 的核心模式,其核心思想是:在不暴露对象内部状态的前提下,捕获并保存对象的当前状态,以便在未来某个时刻将对象恢复到该状态。
简单说:“像游戏存档一样,打完一关后保存当前进度(状态),如果后续失败,可加载存档回到之前的状态,且存档过程不会暴露游戏内部的细节(如怪物血量、玩家装备)”。
日常生活中,文档编辑器的 “撤销(Ctrl+Z)” 功能(保存每次编辑前的状态)、操作系统的 “系统还原点”(保存系统配置状态)、数据库的事务日志(保存操作前的数据状态以便回滚),都是备忘录模式的典型体现。
二、为什么需要备忘录模式?(作用)
当系统中的对象(如文档、游戏角色)需要支持 “状态回退”(如撤销操作、错误恢复)时,直接让客户端保存对象状态会导致:
- 破坏对象封装性:对象需暴露内部状态(如提供大量 getter),客户端才能保存,违反 “封装原则”;
- 状态管理混乱:客户端需手动记录状态的历史版本,逻辑分散,易出错;
- 状态一致性差:若对象状态包含多个关联属性(如游戏角色的血量、魔法值、位置),客户端可能漏存部分状态,导致恢复后数据不一致。
备忘录模式的核心作用是:
- 保护对象封装性:通过备忘录间接保存状态,对象无需暴露内部细节(如私有属性);
- 实现状态回退:安全存储对象的历史状态,支持在需要时(如操作错误)恢复到任意历史版本;
- 集中管理状态:通过 “负责人” 统一管理备忘录,避免状态管理逻辑分散在客户端;
- 保证状态一致性:备忘录由对象自身创建,确保完整保存所有必要的状态信息。
三、反例:直接保存状态的问题
假设我们要实现一个简单的文本编辑器,支持 “输入文本” 和 “撤销” 功能(撤销到上一次输入前的状态)。
不使用备忘录模式的实现:
// 文本编辑器类(需被保存状态的对象)
class TextEditor {
private String content; // 文本内容(内部状态)
public TextEditor() {
this.content = "";
}
// 输入文本(修改状态)
public void type(String text) {
this.content += text;
}
// 为了让客户端保存状态,必须暴露内部内容(破坏封装)
public String getContent() {
return content;
}
// 为了让客户端恢复状态,必须提供设置方法(进一步破坏封装)
public void setContent(String content) {
this.content = content;
}
// 显示当前内容
public void show() {
System.out.println("当前文本:" + content);
}
}
// 客户端:手动管理状态(问题核心)
public class Client {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
// 客户端需手动保存历史状态(如用字符串变量)
String lastState = "";
// 第一次输入
lastState = editor.getContent(); // 保存当前状态(依赖getter,破坏封装)
editor.type("Hello ");
editor.show(); // 输出:当前文本:Hello
// 第二次输入
lastState = editor.getContent(); // 覆盖上次状态
editor.type("World!");
editor.show(); // 输出:当前文本:Hello World!
// 撤销:客户端手动恢复(依赖setter,破坏封装)
editor.setContent(lastState);
editor.show(); // 输出:当前文本:Hello
// 问题:若文本编辑器有更多状态(如字体、颜色),客户端需保存所有属性,逻辑臃肿
// 问题:暴露getter/setter,外部可随意修改状态,破坏对象封装性
// 问题:无法保存多个历史版本(如撤销到第一次输入前),需客户端维护复杂的状态列表
}
}
问题分析:
- 破坏封装性:
TextEditor必须提供getContent和setContent方法才能让客户端保存和恢复状态,导致内部状态(content)暴露,外部可随意修改,违背 “封装原则”; - 状态管理复杂:若对象有多个状态属性(如字体、颜色、光标位置),客户端需手动保存所有属性,代码臃肿且易漏存;
- 无法支持多版本回退:客户端仅能保存 “上一次” 状态,若需撤销到更早的版本(如第三次输入前),需手动维护状态列表(如
List<String>),逻辑分散且易出错; - 耦合度高:客户端与
TextEditor的内部状态强耦合(如依赖content的类型),若TextEditor修改状态属性(如content改为List<String>),客户端代码需同步修改。
四、正例:用备忘录模式解决问题
核心改进:引入 “备忘录” 存储对象状态,“原发器” 负责创建和恢复备忘录,“负责人” 管理备忘录历史,客户端通过负责人间接操作,无需接触对象内部状态。
备忘录模式的实现:
import java.util.ArrayList;
import java.util.List;
// 1. 备忘录(Memento):存储原发器的状态,仅允许原发器访问
// 注意:备忘录通常设为原发器的内部类,或通过权限控制限制访问(如包私有)
class TextMemento {
// 存储的状态(对应原发器的content)
private final String content;
// 构造函数:仅允许原发器调用(通过访问权限控制)
TextMemento(String content) {
this.content = content;
}
// 仅允许原发器获取状态(限制外部访问)
String getContent() {
return content;
}
}
// 2. 原发器(Originator):创建备忘录保存自身状态,或从备忘录恢复状态
class TextEditor {
private String content; // 内部状态(无需暴露getter/setter)
public TextEditor() {
this.content = "";
}
// 输入文本(修改状态)
public void type(String text) {
this.content += text;
}
// 创建备忘录:保存当前状态(仅原发器能创建,确保状态完整)
public TextMemento save() {
return new TextMemento(content);
}
// 从备忘录恢复状态(仅原发器能恢复,确保状态正确)
public void restore(TextMemento memento) {
this.content = memento.getContent();
}
// 显示当前内容
public void show() {
System.out.println("当前文本:" + content);
}
}
// 3. 负责人(Caretaker):管理备忘录历史,不操作备忘录内容
class Caretaker {
// 存储多个历史备忘录(支持多版本回退)
private final List<TextMemento> mementos = new ArrayList<>();
// 保存备忘录
public void saveMemento(TextMemento memento) {
mementos.add(memento);
}
// 获取指定版本的备忘录(如倒数第2个版本)
public TextMemento getMemento(int index) {
return mementos.get(index);
}
// 获取最后一个版本(最近一次保存)
public TextMemento getLastMemento() {
if (mementos.isEmpty()) {
throw new IllegalStateException("无历史记录");
}
return mementos.get(mementos.size() - 1);
}
}
// 4. 客户端:通过负责人管理备忘录,不直接操作状态
public class Client {
public static void main(String[] args) {
// 创建原发器(文本编辑器)和负责人(状态管理器)
TextEditor editor = new TextEditor();
Caretaker caretaker = new Caretaker();
// 初始状态:保存一次
caretaker.saveMemento(editor.save()); // 保存空文本状态
editor.show(); // 输出:当前文本:
// 第一次输入:先保存当前状态,再输入
caretaker.saveMemento(editor.save()); // 保存输入前的状态(空)
editor.type("Hello ");
editor.show(); // 输出:当前文本:Hello
// 第二次输入:保存当前状态(Hello ),再输入
caretaker.saveMemento(editor.save());
editor.type("World!");
editor.show(); // 输出:当前文本:Hello World!
// 撤销1次:恢复到上一次保存的状态(Hello )
editor.restore(caretaker.getLastMemento()); // 获取第二次输入前的状态
editor.show(); // 输出:当前文本:Hello
// 再撤销1次:恢复到第一次输入前的状态(空)
// 移除最后一次保存的状态(第二次输入前),获取更早的版本
caretaker.mementos.remove(caretaker.mementos.size() - 1);
editor.restore(caretaker.getLastMemento());
editor.show(); // 输出:当前文本:
}
}
改进效果:
- 保护封装性:
TextEditor无需提供getContent和setContent等暴露内部状态的方法,状态的保存和恢复通过save和restore完成,且备忘录的getContent仅对原发器可见(通过访问权限控制),外部无法直接修改状态; - 支持多版本回退:
Caretaker通过List管理所有历史备忘录,客户端可恢复到任意历史版本(如第一次输入前、第二次输入前),解决了反例中 “只能撤销一次” 的问题; - 状态管理集中:状态的保存和恢复逻辑由
TextEditor(原发器)和Caretaker(负责人)承担,客户端无需关心细节,只需调用saveMemento和restore,代码简洁清晰; - 状态一致性:备忘录由原发器自身创建(
save方法),确保完整保存所有必要状态(若TextEditor新增 “字体” 属性,只需修改TextMemento和save方法,客户端无需感知); - 低耦合:客户端仅依赖
TextEditor和Caretaker的接口,与TextEditor的内部状态(content)完全解耦,即使状态属性变更,客户端代码也无需修改。
五、备忘录模式的核心结构
备忘录模式通过 “三方协作” 实现状态的安全管理,包含 3 个核心角色:
- 备忘录(Memento):
- 存储原发器的内部状态(如
TextMemento的content),状态的结构与原发器一致; - 限制访问权限:仅允许原发器访问其内部状态(如通过私有构造函数、内部类或包权限),外部(包括负责人)无法修改或直接获取状态,确保封装性;
- 通常是 “只读” 的,状态一旦创建不可修改(如
content设为final)。
- 存储原发器的内部状态(如
- 原发器(Originator):
- 是需要保存状态的对象(如
TextEditor),包含可变化的内部状态; - 提供
save方法:创建备忘录,将当前状态写入备忘录(确保状态完整); - 提供
restore方法:从备忘录中读取状态,恢复自身到之前的状态; - 是唯一能创建和访问备忘录的角色,保证状态的安全性。
- 是需要保存状态的对象(如
- 负责人(Caretaker):
- 管理备忘录的历史记录(如
Caretaker的List<TextMemento>),提供保存、获取备忘录的接口; - 不关心备忘录的内容,也不操作状态(仅存储和传递备忘录),与备忘录的具体结构解耦;
- 负责维护备忘录的生命周期(如限制历史版本数量,避免内存溢出)。
- 管理备忘录的历史记录(如
六、备忘录模式的工作原理
备忘录模式的核心是 “状态的封装与传递”:
- 保存状态:
- 当原发器需要保存状态时(如文本编辑器输入前),调用
save方法创建备忘录(TextMemento),备忘录中包含当前所有状态; - 负责人调用
saveMemento,将备忘录存入历史列表。
- 当原发器需要保存状态时(如文本编辑器输入前),调用
- 恢复状态:
- 当需要回退时,客户端通过负责人获取指定的备忘录(如
getLastMemento); - 原发器调用
restore方法,传入备忘录,从备忘录中读取状态并恢复自身。
- 当需要回退时,客户端通过负责人获取指定的备忘录(如
这种机制保证了:
- 状态的保存和恢复不依赖外部对原发器内部的了解(封装性);
- 历史状态集中管理,支持多版本回退;
- 原发器与负责人解耦(负责人仅传递备忘录,不处理状态)。
七、备忘录模式的优缺点
优点:
- 保护对象封装性:不暴露原发器的内部状态,通过备忘录间接保存和恢复,符合封装原则;
- 支持状态回退:可灵活恢复到任意历史状态,满足撤销、回滚等需求;
- 状态管理集中:负责人统一管理备忘录,避免状态逻辑分散在客户端;
- 状态一致性高:备忘录由原发器创建,确保完整保存所有必要状态,恢复后数据一致。
缺点:
- 资源消耗大:若原发器状态复杂(如大对象、多属性)或历史版本多,备忘录会占用大量内存;
- 备忘录与原发器耦合:备忘录的结构依赖原发器的状态(如
TextMemento依赖TextEditor的content),若原发器状态变更,备忘录需同步修改; - 负责人管理成本:需设计合理的备忘录清理机制(如限制最大版本数),否则可能导致内存泄漏。
八、适用场景
备忘录模式适用于 “需要保存对象历史状态并支持回退” 的场景:
- 撤销操作:文本编辑器、图像编辑软件(如 Photoshop 的历史记录)、代码编辑器的撤销功能;
- 存档与读档:游戏存档(保存角色状态、地图进度)、视频播放器的断点续播(保存播放位置、音量);
- 事务回滚:数据库事务(保存操作前的数据状态,失败时回滚)、分布式系统的状态一致性恢复;
- 快照备份:操作系统的系统还原点、虚拟机的快照功能(保存系统当前状态)。
典型案例:
- Java 中的
java.util.Date(通过clone方法创建状态副本,类似备忘录); - 游戏引擎中的存档系统(如 Unity 的
PlayerPrefs扩展实现状态保存); - 编辑器的历史记录管理器(如 VS Code 的撤销栈)。
九、备忘录模式 vs 原型模式
两者都涉及 “对象状态的复制”,但核心目标和实现方式不同:
| 对比维度 | 备忘录模式 | 原型模式 |
|---|---|---|
| 核心目标 | 保存对象历史状态,支持回退 | 创建对象的副本,实现快速复制 |
| 状态使用场景 | 用于恢复到过去的状态 | 用于创建新对象(与原型对象状态相同) |
| 访问控制 | 备忘录状态仅允许原发器访问(保护封装) | 原型的复制需暴露内部状态(如通过clone或序列化) |
| 管理方式 | 由负责人管理历史版本列表 | 通常不管理副本,由客户端直接使用 |
十、总结
备忘录模式的核心是 “安全保存状态,灵活恢复历史”,通过备忘录、原发器和负责人的三方协作,在不破坏封装性的前提下,实现了对象状态的保存与回退,解决了直接暴露状态导致的封装破坏和管理混乱问题。
它的关键是备忘录的访问权限控制(仅允许原发器操作)和负责人对历史状态的集中管理。实际开发中,当系统需要支持撤销、存档、回滚等功能时,备忘录模式是平衡 “状态可访问性” 与 “封装安全性” 的最佳选择。
记住:备忘录模式是对象状态的 “时光机”,让系统可以 “回到过去”,且不暴露 “时光的秘密”。
4 观察者模式(Observer Pattern)
一、什么是观察者模式?
观察者模式是 ** 行为型模式中专注于 “对象间联动通信”** 的核心模式,其核心思想是:定义对象间的 “一对多” 依赖关系,当一个对象(主题)的状态发生变化时,所有依赖它的对象(观察者)会自动收到通知并更新,实现 “状态变化自动联动”。
简单说:“像微信公众号一样,公众号(主题)发布新文章时,所有订阅它的用户(观察者)都会收到推送并查看,用户无需主动刷新,公众号也不用关心具体有多少用户订阅”。
日常生活中,观察者模式无处不在:
- 气象站(主题)检测到温度变化,手机天气 APP、仪表盘显示器(观察者)自动更新数据;
- 股票价格(主题)波动时,所有关注该股票的投资者(观察者)收到提醒;
- 新闻网站(主题)发布突发新闻,订阅该频道的用户邮箱、APP(观察者)同步推送。
二、为什么需要观察者模式?(作用)
当系统中存在 “一个对象变化需要联动多个对象更新” 的场景(如主题与依赖它的多个观察者),直接在主题中硬编码观察者的更新逻辑会导致:
- 主题与观察者强耦合:主题必须知道所有观察者的具体类型,新增观察者需修改主题代码(违反 “开闭原则”);
- 代码冗余:若多个主题需要类似的联动逻辑,重复编写通知代码;
- 灵活性差:无法动态添加 / 移除观察者(如运行时取消订阅);
- 维护困难:观察者的更新逻辑分散在主题中,修改某一观察者的行为需改动主题。
观察者模式的核心作用是:
- 解耦主题与观察者:主题只依赖观察者接口,无需知道具体实现,新增观察者无需修改主题;
- 支持动态联动:运行时可动态添加 / 移除观察者(如订阅 / 取消订阅),灵活响应变化;
- 实现广播通信:主题状态变化时,自动通知所有观察者,无需逐个调用;
- 符合开闭原则:扩展新的观察者只需实现接口,不影响现有主题和其他观察者。
三、反例:硬编码联动的问题
假设我们要实现一个简单的气象站系统:气象站(WeatherStation)检测温度变化,需要联动两个显示设备 —— 手机 APP(PhoneDisplay)和仪表盘(Dashboard),实时显示最新温度。
不使用观察者模式的实现:
// 1. 手机APP显示设备
class PhoneDisplay {
public void update(int temperature) {
System.out.println("手机APP显示:当前温度 " + temperature + "℃");
}
}
// 2. 仪表盘显示设备
class Dashboard {
public void update(int temperature) {
System.out.println("仪表盘显示:当前温度 " + temperature + "℃");
}
}
// 3. 气象站(主题):直接依赖具体观察者(问题核心)
class WeatherStation {
private int temperature;
// 硬编码依赖具体观察者,无法动态变更
private PhoneDisplay phoneDisplay = new PhoneDisplay();
private Dashboard dashboard = new Dashboard();
// 温度变化时,手动调用所有观察者的更新方法
public void setTemperature(int temperature) {
this.temperature = temperature;
// 必须知道所有观察者的存在,逐个通知
phoneDisplay.update(temperature);
dashboard.update(temperature);
}
}
// 客户端:使用气象站
public class Client {
public static void main(String[] args) {
WeatherStation station = new WeatherStation();
station.setTemperature(25); // 温度变化,联动显示
// 输出:
// 手机APP显示:当前温度 25℃
// 仪表盘显示:当前温度 25℃
// 问题:新增观察者(如网页显示WebDisplay)需修改WeatherStation,添加新的成员变量和update调用
// 问题:无法动态取消某一观察者(如用户关闭手机APP,仍会收到通知)
// 问题:气象站与具体观察者强耦合,若PhoneDisplay的update方法名变更,WeatherStation需同步修改
}
}
问题分析:
- 违反开闭原则:新增显示设备(如
WebDisplay)时,必须修改WeatherStation的代码(添加WebDisplay成员变量,并在setTemperature中调用其update方法),原有逻辑面临被破坏的风险; - 强耦合:
WeatherStation直接依赖PhoneDisplay和Dashboard的具体实现,而非抽象接口,一旦观察者类名或方法名变更,主题必须同步修改; - 灵活性差:无法在运行时动态添加 / 移除观察者(如用户临时关闭仪表盘,气象站仍会向其发送通知);
- 逻辑分散:通知逻辑(“谁需要被通知”)嵌入在主题中,若多个主题需要类似逻辑,需重复编写,维护成本高。
四、正例:用观察者模式解决问题
核心改进:定义 “主题接口” 和 “观察者接口”,主题只管理观察者接口列表,状态变化时通知所有注册的观察者;观察者实现更新接口,按需注册到主题,实现 “主题与观察者解耦,动态联动”。
观察者模式的实现:
import java.util.ArrayList;
import java.util.List;
// 1. 观察者接口(Observer):定义更新方法,所有观察者需实现
interface Observer {
void update(int temperature); // 接收主题的状态更新
}
// 2. 主题接口(Subject):定义管理观察者的方法(注册、移除、通知)
interface Subject {
void registerObserver(Observer observer); // 注册观察者
void removeObserver(Observer observer); // 移除观察者
void notifyObservers(); // 通知所有观察者
}
// 3. 具体主题(Concrete Subject):气象站,实现主题接口
class WeatherStation implements Subject {
private int temperature; // 主题的状态(温度)
// 维护观察者列表(依赖接口,不依赖具体实现)
private List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer); // 注册观察者
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer); // 移除观察者
}
@Override
public void notifyObservers() {
// 遍历所有观察者,调用其更新方法(多态)
for (Observer observer : observers) {
observer.update(temperature);
}
}
// 温度变化时,触发通知
public void setTemperature(int temperature) {
this.temperature = temperature;
notifyObservers(); // 状态变化,通知所有观察者
}
}
// 4. 具体观察者1:手机APP
class PhoneDisplay implements Observer {
@Override
public void update(int temperature) {
System.out.println("手机APP显示:当前温度 " + temperature + "℃");
}
}
// 5. 具体观察者2:仪表盘
class Dashboard implements Observer {
@Override
public void update(int temperature) {
System.out.println("仪表盘显示:当前温度 " + temperature + "℃");
}
}
// 6. 新增具体观察者3:网页显示(无需修改主题)
class WebDisplay implements Observer {
@Override
public void update(int temperature) {
System.out.println("网页显示:当前温度 " + temperature + "℃");
}
}
// 7. 客户端:动态管理观察者
public class Client {
public static void main(String[] args) {
// 创建主题(气象站)
WeatherStation station = new WeatherStation();
// 创建观察者
Observer phone = new PhoneDisplay();
Observer dashboard = new Dashboard();
Observer web = new WebDisplay();
// 注册观察者(订阅)
station.registerObserver(phone);
station.registerObserver(dashboard);
// 温度变化:通知已注册的观察者(手机和仪表盘)
System.out.println("=== 温度变为25℃ ===");
station.setTemperature(25);
// 输出:
// 手机APP显示:当前温度 25℃
// 仪表盘显示:当前温度 25℃
// 动态添加新观察者(网页)
station.registerObserver(web);
System.out.println("\n=== 温度变为30℃ ===");
station.setTemperature(30);
// 输出:
// 手机APP显示:当前温度 30℃
// 仪表盘显示:当前温度 30℃
// 网页显示:当前温度 30℃
// 动态移除观察者(仪表盘)
station.removeObserver(dashboard);
System.out.println("\n=== 温度变为28℃ ===");
station.setTemperature(28);
// 输出:
// 手机APP显示:当前温度 28℃
// 网页显示:当前温度 28℃
}
}
改进效果:
- 符合开闭原则:新增观察者(如
WebDisplay)时,只需实现Observer接口,无需修改WeatherStation(主题)的代码,扩展安全且灵活; - 解耦主题与观察者:主题(
WeatherStation)只依赖Observer接口,不关心具体观察者类型,观察者也无需知道主题的实现,两者通过接口交互,耦合度极低; - 动态管理观察者:支持运行时注册(
registerObserver)和移除(removeObserver)观察者(如用户关闭仪表盘后不再接收通知),灵活性远超硬编码; - 广播通信自动化:主题状态变化时,通过
notifyObservers自动通知所有注册的观察者,无需手动逐个调用,减少代码冗余; - 职责清晰:主题专注于状态管理和通知,观察者专注于接收通知后的处理(如显示逻辑),符合 “单一职责原则”。
五、观察者模式的核心结构
观察者模式通过 “接口定义 + 动态管理” 实现对象间的联动,包含 4 个核心角色:
- 主题(Subject):
- 定义管理观察者的接口,包括
registerObserver(注册)、removeObserver(移除)、notifyObservers(通知); - 持有观察者列表(依赖
Observer接口,不依赖具体实现); - 当自身状态变化时,调用
notifyObservers通知所有注册的观察者。
- 定义管理观察者的接口,包括
- 具体主题(Concrete Subject):
- 实现
Subject接口,是具体的状态持有者(如WeatherStation的temperature); - 状态变化时(如
setTemperature),触发notifyObservers方法,向观察者发送更新; - 负责维护观察者列表的具体实现(如用
List存储)。
- 实现
- 观察者(Observer):
- 定义接收通知的接口(如
update方法),声明观察者如何响应主题的状态变化; - 方法参数通常包含主题的状态信息(如温度),或允许观察者主动从主题获取状态(按需设计)。
- 定义接收通知的接口(如
- 具体观察者(Concrete Observer):
- 实现
Observer接口,是依赖主题的对象(如PhoneDisplay、Dashboard); - 注册到具体主题后,当主题通知时,执行
update方法处理状态(如显示温度); - 可持有主题的引用(可选),以便主动获取最新状态(如
update方法未传递足够信息时)。
- 实现
六、观察者模式的工作原理
观察者模式的核心是 “注册 - 通知 - 更新” 的联动机制:
- 注册:观察者通过主题的
registerObserver方法订阅主题(如手机 APP 订阅气象站),主题将观察者加入列表; - 通知:当主题状态变化时(如温度从 25℃变为 30℃),调用
notifyObservers遍历观察者列表,逐个调用观察者的update方法; - 更新:观察者通过
update方法接收状态信息,执行自身逻辑(如手机 APP 显示新温度)。
这种机制保证了:
- 主题与观察者的解耦(通过接口交互,互不依赖具体实现);
- 观察者的动态性(可随时加入或退出观察);
- 状态变化的自动传播(无需人工干预,主题主动推送)。
七、观察者模式的优缺点
优点:
- 解耦主题与观察者:两者通过接口交互,互不依赖具体实现,降低耦合度;
- 支持动态扩展:新增观察者无需修改主题,符合开闭原则,灵活性高;
- 自动联动更新:主题状态变化时,所有观察者自动收到通知,无需手动协调;
- 广播通信高效:一次通知触发所有观察者更新,适合 “一对多” 场景的批量处理。
缺点:
- 通知顺序不确定:默认情况下,观察者的更新顺序与注册顺序一致,但主题不保证顺序,若观察者间有依赖可能导致问题;
- 过度通知风险:若主题频繁变化,会触发大量观察者更新,可能影响性能;
- 循环依赖问题:若观察者更新时又触发主题状态变化,可能导致无限循环(需谨慎设计);
- 观察者过多时维护难:大量观察者注册后,排查 “谁收到了通知” 或 “谁没收到” 的问题较复杂。
八、适用场景
观察者模式适用于 “一个对象变化需要联动多个对象更新” 的场景,尤其是 “一对多” 依赖关系:
- 事件监听系统:按钮点击事件(主题)触发多个回调函数(观察者)、鼠标移动事件联动界面元素更新;
- 消息订阅场景:公众号推送、邮件订阅、RSS 订阅(主题发布内容,订阅者接收);
- 数据同步场景:分布式系统中的数据一致性(主节点数据变化,从节点同步更新)、实时仪表盘(数据源变化,多视图同步刷新);
- 状态联动场景:游戏中角色血量变化(主题),触发 UI 血条、音效、任务进度(观察者)的联动更新。
典型案例:
- Java 中的
java.util.Observer和Observable(JDK 内置的观察者模式实现); - Android 的
View.OnClickListener(按钮点击事件的观察者模式应用); - Spring 的
ApplicationEvent和ApplicationListener(事件发布与监听机制); - 前端的
addEventListener(DOM 事件的观察者模式实现)。
九、观察者模式 vs 发布 - 订阅模式
两者常被混淆,核心区别在于是否有 “中间层”:
| 对比维度 | 观察者模式 | 发布 - 订阅模式 |
|---|---|---|
| 核心结构 | 主题直接通知观察者(无中间层) | 发布者→中间代理(如消息队列)→订阅者(有中间层) |
| 耦合度 | 主题与观察者轻度耦合(通过接口) | 发布者与订阅者完全解耦(互不感知) |
| 适用场景 | 进程内的对象联动(如同一应用内的 UI 与数据) | 跨进程 / 跨系统的通信(如分布式系统、消息通知) |
| 灵活性 | 适合简单的 “一对多” 联动 | 支持更复杂的路由、过滤、异步通信 |
注:发布 - 订阅模式可视为观察者模式的 “进阶版”,通过中间代理进一步解耦,适用于更复杂的分布式场景。
十、总结
观察者模式的核心是 “状态联动,解耦通信”,通过定义主题与观察者的接口,实现了 “一个对象变化,多个对象自动响应” 的机制,解决了硬编码联动导致的耦合高、扩展难问题。
它的关键是主题对观察者接口的依赖(而非具体实现)和动态管理观察者列表的能力。实际开发中,当遇到 “一对多” 的状态联动需求时,观察者模式是实现灵活、低耦合系统的最佳实践。
记住:观察者模式是对象间的 “自动联络员”,让状态变化的 “消息” 能按需传递,而无需 “亲自跑腿”。
5 模板方法模式(Template Method Pattern)
一、什么是模板方法模式?
模板方法模式是 行为型模式中专注于 “算法骨架复用与步骤标准化”的核心模式,其核心思想是:在抽象父类中定义一个算法的 “骨架流程”(模板方法),将算法中可变的具体步骤延迟到子类中实现,使得子类在不改变算法整体结构的前提下,仅能重写算法的特定步骤。
简单说:“像做蛋糕的食谱一样,食谱(父类)定义了‘准备原料→搅拌→烘烤→装饰’的固定流程(模板方法),不同口味的蛋糕(子类)只需改变‘原料’和‘装饰’步骤,无需修改整体流程”。
日常生活中,模板方法模式无处不在:
- 泡茶与冲咖啡:都遵循 “烧开水→放原料→冲泡→加配料” 的固定流程,仅 “原料”(茶叶 / 咖啡粉)和 “配料”(糖 / 奶)不同;
- 考试答题:都遵循 “看题→审题→答题→检查” 的流程,仅 “答题” 步骤的具体内容不同;
- 框架中的流程模板:如 Spring 的
JdbcTemplate,定义了 “获取连接→创建语句→执行→关闭资源” 的数据库操作骨架,仅 “执行 SQL” 步骤由用户实现。
二、为什么需要模板方法模式?(作用)
当系统中存在 ** 多个算法 “流程结构完全相同,但部分步骤实现不同”** 时,直接为每个算法编写完整流程会导致:
- 代码冗余:相同的流程步骤(如烧开水、关闭资源)在多个算法中重复编写;
- 步骤混乱:不同算法可能因开发者疏忽改变流程顺序(如先放茶叶再烧开水),导致逻辑错误;
- 维护困难:若需修改流程(如新增 “消毒” 步骤),需在所有算法中同步修改,违反 “开闭原则”;
- 一致性差:不同算法的流程结构不统一,不利于团队协作和代码理解。
模板方法模式的核心作用是:
- 复用算法骨架:将相同流程抽离到父类,子类仅实现可变步骤,减少代码冗余;
- 标准化流程顺序:父类模板方法固定步骤顺序,避免子类随意修改流程,保证一致性;
- 支持灵活扩展:子类通过重写可变步骤扩展功能,无需修改父类流程,符合 “开闭原则”;
- 简化子类实现:子类无需关注整体流程,只需专注于自身的具体步骤,降低开发难度。
三、反例:重复流程导致的代码冗余问题
假设我们要实现 “泡茶” 和 “冲咖啡” 两个功能,两者流程相似但部分步骤不同。
不使用模板方法模式的实现:
// 1. 泡茶类(完整流程,包含重复步骤)
class Tea {
// 泡茶完整流程(步骤固定,但与咖啡重复)
public void make() {
boilWater(); // 烧开水(重复)
addTea(); // 放茶叶(特有)
brew(); // 冲泡(重复逻辑)
addSugar(); // 加糖(特有)
System.out.println("茶泡好了");
}
private void boilWater() { System.out.println("烧开水"); }
private void addTea() { System.out.println("放入茶叶"); }
private void brew() { System.out.println("浸泡5分钟"); }
private void addSugar() { System.out.println("加入冰糖"); }
}
// 2. 冲咖啡类(完整流程,重复步骤多)
class Coffee {
// 冲咖啡完整流程(与泡茶流程结构相同,但重复编写)
public void make() {
boilWater(); // 烧开水(重复)
addCoffee(); // 放咖啡粉(特有)
brew(); // 冲泡(重复逻辑)
addMilk(); // 加牛奶(特有)
System.out.println("咖啡冲好了");
}
private void boilWater() { System.out.println("烧开水"); }
private void addCoffee() { System.out.println("放入咖啡粉"); }
private void brew() { System.out.println("浸泡3分钟"); }
private void addMilk() { System.out.println("加入牛奶"); }
}
// 客户端:使用两个类
public class Client {
public static void main(String[] args) {
new Tea().make();
// 输出:
// 烧开水
// 放入茶叶
// 浸泡5分钟
// 加入冰糖
// 茶泡好了
new Coffee().make();
// 输出:
// 烧开水
// 放入咖啡粉
// 浸泡3分钟
// 加入牛奶
// 咖啡冲好了
// 问题:新增“冲奶茶”需重复编写烧开水、冲泡等步骤,代码冗余
// 问题:修改流程(如新增“洗杯子”)需修改所有类,维护成本高
// 问题:子类可能乱改步骤顺序(如咖啡先加牛奶再冲泡),逻辑错误
}
}
问题分析:
- 代码冗余严重:“烧开水” 等相同步骤在
Tea和Coffee中重复编写,若新增 “冲奶茶”“冲可可”,重复代码会进一步增多; - 流程一致性差:子类可随意修改
make方法的步骤顺序(如咖啡先加牛奶再冲泡),导致逻辑错误; - 维护成本高:若需新增流程步骤(如 “洗杯子”),需在所有类的
make方法中同步添加,违反 “开闭原则”; - 子类开发复杂:新子类需重新编写完整流程,开发者需记住所有步骤,易遗漏或出错。
四、正例:用模板方法模式解决问题
核心改进:创建抽象父类,定义 “算法骨架”(模板方法),将相同步骤在父类中实现,可变步骤声明为抽象方法让子类实现,统一流程并复用代码。
模板方法模式的实现:
// 1. 抽象父类(Abstract Class):定义算法骨架和步骤
abstract class Beverage {
// 模板方法:定义固定流程(核心,不可被子类修改)
public final void make() {
washCup(); // 新增步骤:洗杯子(统一添加,所有子类自动继承)
boilWater(); // 步骤1:烧开水(父类实现,复用)
addIngredient(); // 步骤2:加原料(抽象方法,子类实现)
brew(); // 步骤3:冲泡(抽象方法,子类实现)
addCondiment(); // 步骤4:加配料(抽象方法,子类实现)
finish(); // 步骤5:完成(父类实现,复用)
}
// 具体方法:所有子类共用的步骤(父类实现)
private void washCup() {
System.out.println("洗干净杯子");
}
private void boilWater() {
System.out.println("烧开水");
}
private void finish() {
System.out.println("饮品做好了\n");
}
// 抽象方法:子类特有步骤(延迟到子类实现)
protected abstract void addIngredient(); // 加原料(茶叶/咖啡粉)
protected abstract void brew(); // 冲泡(浸泡时间不同)
protected abstract void addCondiment(); // 加配料(糖/牛奶)
}
// 2. 具体子类1:泡茶(实现抽象步骤)
class Tea extends Beverage {
@Override
protected void addIngredient() {
System.out.println("放入茶叶");
}
@Override
protected void brew() {
System.out.println("浸泡5分钟");
}
@Override
protected void addCondiment() {
System.out.println("加入冰糖");
}
}
// 3. 具体子类2:冲咖啡(实现抽象步骤)
class Coffee extends Beverage {
@Override
protected void addIngredient() {
System.out.println("放入咖啡粉");
}
@Override
protected void brew() {
System.out.println("浸泡3分钟");
}
@Override
protected void addCondiment() {
System.out.println("加入牛奶");
}
}
// 4. 新增子类3:冲奶茶(无需修改父类,直接扩展)
class MilkTea extends Beverage {
@Override
protected void addIngredient() {
System.out.println("放入红茶包");
}
@Override
protected void brew() {
System.out.println("浸泡8分钟");
}
@Override
protected void addCondiment() {
System.out.println("加入鲜奶和珍珠");
}
}
// 5. 客户端:使用子类,无需关心流程
public class Client {
public static void main(String[] args) {
Beverage tea = new Tea();
tea.make();
// 输出:
// 洗干净杯子
// 烧开水
// 放入茶叶
// 浸泡5分钟
// 加入冰糖
// 饮品做好了
Beverage coffee = new Coffee();
coffee.make();
// 输出:
// 洗干净杯子
// 烧开水
// 放入咖啡粉
// 浸泡3分钟
// 加入牛奶
// 饮品做好了
Beverage milkTea = new MilkTea();
milkTea.make();
// 输出:
// 洗干净杯子
// 烧开水
// 放入红茶包
// 浸泡8分钟
// 加入鲜奶和珍珠
// 饮品做好了
}
}
改进效果:
- 代码复用彻底:“洗杯子”“烧开水”“完成” 等相同步骤在父类中实现,子类无需重复编写,新增
MilkTea时仅需实现 3 个特有步骤; - 流程绝对统一:模板方法
make被final修饰,子类无法修改步骤顺序(如不能先加配料再烧开水),确保所有饮品流程一致; - 符合开闭原则:新增饮品(如 “冲可可”)只需新增子类,父类无需修改;修改流程(如新增 “消毒”)只需在父类
make中添加,所有子类自动继承; - 子类实现简化:子类仅需关注自身的特有步骤(加原料、冲泡、加配料),无需关心整体流程,降低开发难度;
- 维护成本降低:相同逻辑集中在父类,修改时只需改一处,避免遗漏。
五、模板方法模式的核心结构
模板方法模式通过 “父类定骨架,子类填细节” 实现算法复用,包含 2 个核心角色:
- 抽象类(Abstract Class):
- 定义算法骨架(模板方法):通常用
final修饰,确保子类无法修改流程顺序(如Beverage的make方法); - 实现共用步骤(具体方法):所有子类都相同的步骤(如
washCup、boilWater),在父类中实现以复用; - 声明可变步骤(抽象方法 / 钩子方法):子类需实现的特有步骤(如
addIngredient),或可选重写的步骤(钩子方法)。
- 定义算法骨架(模板方法):通常用
- 具体子类(Concrete Class):
- 继承抽象类,实现抽象方法:完成自身特有的步骤(如
Tea实现 “放茶叶”“加冰糖”); - 可选重写钩子方法(若抽象类提供):根据需求调整算法流程(如是否加配料);
- 不修改模板方法:仅通过实现 / 重写步骤扩展功能,不改变算法整体结构。
- 继承抽象类,实现抽象方法:完成自身特有的步骤(如
关键:模板方法 vs 抽象方法 vs 钩子方法
- 模板方法:父类中
final修饰的方法,定义算法步骤顺序,是核心; - 抽象方法:父类中未实现的方法(
abstract),子类必须实现,对应 “必须不同” 的步骤; - 钩子方法:父类中提供默认实现的方法,子类可选择重写(如 “是否加配料”),对应 “可选不同” 的步骤。
钩子方法示例(扩展上述代码):
abstract class Beverage {
// 模板方法
public final void make() {
boilWater();
addIngredient();
brew();
if (needCondiment()) { // 钩子方法控制是否执行加配料步骤
addCondiment();
}
finish();
}
// 钩子方法:默认需要加配料,子类可重写
protected boolean needCondiment() {
return true;
}
// 其他方法不变...
}
// 无糖茶子类:重写钩子方法,不需要加配料
class SugarFreeTea extends Beverage {
@Override
protected boolean needCondiment() {
return false; // 不执行addCondiment
}
// 其他实现不变...
}
六、模板方法模式的工作原理
模板方法模式的核心是 “骨架固定,细节延迟”,遵循 “好莱坞原则”(“别找我们,我们会找你”):
- 父类定义骨架:抽象类的模板方法按固定顺序调用 “共用步骤” 和 “可变步骤”,明确算法的整体流程;
- 子类实现细节:具体子类仅需实现抽象方法(或重写钩子方法),无需关心步骤顺序;
- 客户端触发执行:客户端创建子类实例,调用模板方法,父类自动按流程执行共用步骤和子类的特有步骤。
这种机制保证了:
- 算法流程的一致性(父类控制);
- 代码的复用性(共用步骤集中);
- 功能的扩展性(子类灵活实现细节)。
七、模板方法模式的优缺点
优点:
- 代码复用率高:相同步骤集中在父类,避免重复编写,减少冗余;
- 流程标准化:模板方法固定步骤顺序,防止子类乱改流程导致逻辑错误;
- 符合开闭原则:新增功能只需新增子类,无需修改父类,扩展灵活;
- 简化子类开发:子类无需关注整体流程,仅专注于自身特有步骤,降低开发难度;
- 维护成本低:修改共用逻辑只需改父类,无需修改所有子类。
缺点:
- 子类数量可能增多:每个具体功能都需对应一个子类,若功能过多,可能导致类数量膨胀;
- 父类修改风险高:若父类修改模板方法的步骤顺序,可能影响所有子类的逻辑;
- 灵活性受限:子类只能按父类定义的流程扩展,无法改变步骤顺序(若需改变,需修改父类,违反开闭原则);
- 抽象类与子类耦合度高:子类依赖父类的模板方法和步骤定义,父类接口变更可能导致所有子类修改。
八、适用场景
模板方法模式适用于 “多个算法流程结构相同,仅部分步骤实现不同” 的场景:
- 流程标准化场景:如框架中的流程模板(Spring 的
JdbcTemplate、MyBatis 的BaseMapper)、业务中的审批流程(请假→部门审批→总经理审批,仅 “审批条件” 不同); - 重复步骤较多场景:如做饭(切菜→炒菜→装盘,仅 “食材” 和 “调料” 不同)、文件处理(读取→解析→保存,仅 “解析逻辑” 不同);
- 子类扩展受限场景:需要限制子类只能修改部分步骤,不能改变整体流程(如考试答题流程、生产流水线);
- 团队协作场景:统一算法结构,让不同开发者专注于实现各自负责的步骤(如多人协作开发不同类型的报表,统一 “查询数据→计算→导出” 流程)。
典型案例:
- Java 的
AbstractList(iterator方法为模板方法,get方法为抽象方法,子类实现获取元素的细节); - Spring 的
AbstractApplicationContext(refresh方法为模板方法,定义容器初始化流程,子类实现具体步骤); - 单元测试框架(如 JUnit 的
setUp/tearDown,模板方法定义 “初始化→测试→清理” 流程,用户实现测试步骤)。
九、模板方法模式 vs 策略模式
两者都涉及 “算法的封装与复用”,但核心目标和灵活性不同:
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 核心目标 | 复用算法骨架,标准化流程 | 封装不同算法,支持动态切换 |
| 流程控制 | 父类固定流程顺序,子类不能改变 | 客户端选择不同算法,流程可完全不同 |
| 扩展性 | 扩展子类实现步骤,流程不变 | 扩展策略类,可替换整个算法 |
| 适用场景 | 流程相同、步骤不同(如泡茶 / 咖啡) | 算法不同、目标相同(如支付方式 / 排序) |
十、总结
模板方法模式的核心是 “骨架固定,细节延迟”,通过抽象父类定义算法流程,将可变步骤交给子类实现,既保证了流程的一致性,又实现了代码的复用和灵活扩展。
它的关键是模板方法的final修饰(防止流程被修改)和抽象方法 / 钩子方法的合理设计(区分 “必须不同” 和 “可选不同” 的步骤)。实际开发中,当遇到 “多个功能流程相似但细节不同” 的场景时,模板方法模式是提升代码质量、降低维护成本的最佳实践。
记住:模板方法模式让算法的 “流程” 成为 “模板”,让子类的 “细节” 按需 “填充”。
6 迭代器模式(Iterator Pattern)
一、什么是迭代器模式?
迭代器模式是 行为型模式中专注于 “集合遍历逻辑分离与统一”的核心模式,其核心思想是:提供一种统一的方式遍历不同结构的集合对象(如数组、链表、树),将集合的遍历逻辑从集合对象中分离出来,使客户端无需了解集合的内部结构即可实现遍历。
简单说:“像遥控器换台一样,不管电视里的频道列表是数组、链表还是其他结构,遥控器(迭代器)都能通过‘下一个’‘是否有下一个’按钮统一操作,用户无需知道频道的存储方式”。
日常生活中,迭代器模式的例子随处可见:
- 图书管理员按编号遍历书架上的书(不管书架是层架式还是抽屉式);
- 音乐播放器按列表顺序播放歌曲(不管歌曲列表存在数组还是链表中);
- 手机相册滑动查看照片(不管照片是存在本地数组还是云端链表)。
二、为什么需要迭代器模式?(作用)
当系统中存在多种集合结构(如数组、链表、哈希表),且客户端需要遍历这些集合时,直接让客户端依赖集合的内部结构会导致:
- 客户端与集合强耦合:客户端需知道集合是数组(用
for循环)、链表(用while循环)还是树(用递归),才能编写对应遍历逻辑; - 代码冗余:不同集合的遍历逻辑重复编写(如数组的
i++和链表的node = node.next); - 扩展困难:新增集合类型(如栈)时,客户端需新增对应的遍历代码,违反 “开闭原则”;
- 集合内部结构暴露:客户端需访问集合的内部属性(如数组的
length、链表的head节点),破坏封装性。
迭代器模式的核心作用是:
- 统一遍历接口:用一致的方法(如
hasNext()、next())遍历所有集合,客户端无需区分集合类型; - 分离遍历与集合:遍历逻辑封装在迭代器中,集合只需提供获取迭代器的接口,两者解耦;
- 隐藏集合内部结构:客户端通过迭代器访问元素,无需知道集合是数组、链表还是其他结构,保护封装性;
- 支持多种遍历方式:同一集合可提供多个迭代器(如正序、倒序遍历),客户端按需选择,灵活扩展。
三、反例:直接依赖集合结构的遍历问题
假设我们要实现一个图书管理系统,包含两种书架:数组书架(ArrayBookShelf)和链表书架(LinkedBookShelf),客户端需要遍历这两种书架上的图书。
不使用迭代器模式的实现:
// 1. 图书类
class Book {
private String name;
public Book(String name) { this.name = name; }
public String getName() { return name; }
}
// 2. 数组书架(内部用数组存储)
class ArrayBookShelf {
private Book[] books;
private int size;
public ArrayBookShelf(int capacity) {
this.books = new Book[capacity];
this.size = 0;
}
public void addBook(Book book) {
if (size < books.length) books[size++] = book;
}
// 暴露内部数组和大小(破坏封装,供客户端遍历)
public Book[] getBooks() { return books; }
public int getSize() { return size; }
}
// 3. 链表书架(内部用链表存储)
class LinkedBookShelf {
private Node head;
private int size;
private class Node {
Book book;
Node next;
Node(Book book) { this.book = book; }
}
public LinkedBookShelf() {
this.head = null;
this.size = 0;
}
public void addBook(Book book) {
Node newNode = new Node(book);
if (head == null) head = newNode;
else {
Node current = head;
while (current.next != null) current = current.next;
current.next = newNode;
}
size++;
}
// 暴露内部头节点和大小(破坏封装,供客户端遍历)
public Node getHead() { return head; }
public int getSize() { return size; }
}
// 客户端:遍历两种书架(需知道内部结构,问题核心)
public class Client {
public static void main(String[] args) {
// 数组书架遍历
ArrayBookShelf arrayShelf = new ArrayBookShelf(3);
arrayShelf.addBook(new Book("Java编程"));
arrayShelf.addBook(new Book("设计模式"));
System.out.println("数组书架的书:");
// 客户端必须知道是数组,用for循环遍历(依赖内部结构)
for (int i = 0; i < arrayShelf.getSize(); i++) {
System.out.println(arrayShelf.getBooks()[i].getName());
}
// 链表书架遍历
LinkedBookShelf linkedShelf = new LinkedBookShelf();
linkedShelf.addBook(new Book("数据结构"));
linkedShelf.addBook(new Book("算法导论"));
System.out.println("\n链表书架的书:");
// 客户端必须知道是链表,用while循环遍历(依赖内部结构)
LinkedBookShelf.Node current = linkedShelf.getHead();
while (current != null) {
System.out.println(current.book.getName());
current = current.next;
}
// 问题:新增“哈希表书架”时,客户端需新增哈希表的遍历逻辑(如for-each),违反开闭原则
// 问题:客户端依赖集合的内部结构(数组的getBooks、链表的getHead),破坏封装
// 问题:遍历逻辑重复(不同集合的遍历代码无法复用)
}
}
问题分析:
- 强耦合:客户端必须知道集合的内部结构(数组用
for、链表用while)才能遍历,若集合结构变更(如数组改为动态数组),客户端遍历代码需同步修改; - 封装性破坏:集合需暴露内部属性(如
getBooks()、getHead())供客户端遍历,外部可直接修改这些属性(如arrayShelf.getBooks()[0] = null),违背封装原则; - 扩展困难:新增集合类型(如
HashBookShelf)时,客户端需编写新的遍历逻辑(如for (Book book : hashShelf.getMap().values())),违反 “开闭原则”; - 代码冗余:不同集合的遍历逻辑无法复用,若多个客户端需要遍历,会重复编写类似代码(如数组的
for循环)。
四、正例:用迭代器模式解决问题
核心改进:定义 “迭代器接口” 封装遍历逻辑(hasNext()判断是否有下一个元素,next()获取下一个元素),集合提供 “获取迭代器” 的接口,客户端通过迭代器统一遍历,不依赖集合内部结构。
迭代器模式的实现:
// 1. 图书类(不变)
class Book {
private String name;
public Book(String name) { this.name = name; }
public String getName() { return name; }
}
// 2. 迭代器接口(Iterator):定义统一遍历方法
interface Iterator {
boolean hasNext(); // 是否有下一个元素
Book next(); // 获取下一个元素
}
// 3. 聚合接口(Aggregate):集合的统一接口,提供获取迭代器的方法
interface Aggregate {
Iterator createIterator(); // 创建迭代器
}
// 4. 具体聚合1:数组书架(实现聚合接口)
class ArrayBookShelf implements Aggregate {
private Book[] books;
private int size;
public ArrayBookShelf(int capacity) {
this.books = new Book[capacity];
this.size = 0;
}
public void addBook(Book book) {
if (size < books.length) books[size++] = book;
}
// 提供内部访问方法(仅迭代器可见,不暴露给客户端)
Book getBookAt(int index) {
return books[index];
}
public int getSize() {
return size;
}
// 创建数组对应的迭代器
@Override
public Iterator createIterator() {
return new ArrayIterator(this);
}
// 具体迭代器1:数组迭代器(实现迭代器接口)
private class ArrayIterator implements Iterator {
private ArrayBookShelf shelf;
private int index; // 当前位置
public ArrayIterator(ArrayBookShelf shelf) {
this.shelf = shelf;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < shelf.getSize();
}
@Override
public Book next() {
return shelf.getBookAt(index++);
}
}
}
// 5. 具体聚合2:链表书架(实现聚合接口)
class LinkedBookShelf implements Aggregate {
private Node head;
private int size;
private class Node {
Book book;
Node next;
Node(Book book) { this.book = book; }
}
public LinkedBookShelf() {
this.head = null;
this.size = 0;
}
public void addBook(Book book) {
Node newNode = new Node(book);
if (head == null) head = newNode;
else {
Node current = head;
while (current.next != null) current = current.next;
current.next = newNode;
}
size++;
}
// 创建链表对应的迭代器
@Override
public Iterator createIterator() {
return new LinkedIterator(this);
}
// 具体迭代器2:链表迭代器(实现迭代器接口)
private class LinkedIterator implements Iterator {
private Node current; // 当前节点
public LinkedIterator(LinkedBookShelf shelf) {
this.current = shelf.head;
}
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Book next() {
Book book = current.book;
current = current.next;
return book;
}
}
}
// 6. 客户端:通过迭代器统一遍历,不关心集合类型
public class Client {
public static void main(String[] args) {
// 数组书架
ArrayBookShelf arrayShelf = new ArrayBookShelf(3);
arrayShelf.addBook(new Book("Java编程"));
arrayShelf.addBook(new Book("设计模式"));
// 获取迭代器
Iterator arrayIterator = arrayShelf.createIterator();
System.out.println("数组书架的书:");
// 统一遍历逻辑(hasNext + next)
while (arrayIterator.hasNext()) {
Book book = arrayIterator.next();
System.out.println(book.getName());
}
// 链表书架
LinkedBookShelf linkedShelf = new LinkedBookShelf();
linkedShelf.addBook(new Book("数据结构"));
linkedShelf.addBook(new Book("算法导论"));
// 获取迭代器
Iterator linkedIterator = linkedShelf.createIterator();
System.out.println("\n链表书架的书:");
// 同样的遍历逻辑,无需修改
while (linkedIterator.hasNext()) {
Book book = linkedIterator.next();
System.out.println(book.getName());
}
// 新增哈希表书架:只需实现Aggregate和对应的Iterator,客户端遍历逻辑不变
// HashBookShelf hashShelf = new HashBookShelf();
// Iterator hashIterator = hashShelf.createIterator();
// while (hashIterator.hasNext()) { ... }
}
}
改进效果:
- 统一遍历接口:客户端用相同的
while (iterator.hasNext()) { iterator.next() }逻辑遍历数组和链表书架,无需区分集合类型,代码简洁且一致; - 解耦集合与遍历:遍历逻辑封装在迭代器(
ArrayIterator、LinkedIterator)中,集合(ArrayBookShelf)只需提供createIterator方法,两者互不依赖内部实现; - 保护封装性:集合不再暴露内部结构(如数组的
books、链表的head),仅通过迭代器间接访问元素,外部无法直接修改内部数据; - 符合开闭原则:新增集合类型(如
HashBookShelf)时,只需实现Aggregate接口并提供对应的迭代器,客户端遍历代码无需修改,扩展灵活; - 支持多遍历方式:同一集合可提供多个迭代器(如
ArrayBookShelf新增ReverseArrayIterator实现倒序遍历),客户端按需选择,不影响集合本身。
五、迭代器模式的核心结构
迭代器模式通过 “迭代器封装遍历,聚合提供迭代器” 实现统一遍历,包含 4 个核心角色:
-
迭代器(Iterator):
-
定义
统一的遍历接口
,包含两个核心方法:
hasNext():判断是否还有下一个元素;next():获取下一个元素并移动指针;
-
可选扩展方法(如
remove()删除当前元素),但核心是遍历控制。
-
-
具体迭代器(Concrete Iterator):
- 实现
Iterator接口,封装具体集合的遍历逻辑(如数组的index++、链表的node = node.next); - 持有对具体集合的引用(如
ArrayIterator持有ArrayBookShelf),以便访问集合元素; - 维护遍历的当前位置(如
index、current节点),确保遍历有序进行。
- 实现
-
聚合(Aggregate):
- 定义集合的统一接口,核心是
createIterator()方法,用于创建对应集合的迭代器; - 声明集合的基本操作(如
add、remove),但不包含遍历逻辑。
- 定义集合的统一接口,核心是
-
具体聚合(Concrete Aggregate):
- 实现
Aggregate接口,是具体的集合对象(如ArrayBookShelf、LinkedBookShelf); - 内部维护元素的存储结构(数组、链表等),并实现
createIterator()方法,返回与自身匹配的具体迭代器(如ArrayBookShelf返回ArrayIterator)。
- 实现
六、迭代器模式的工作原理
迭代器模式的核心是 “遍历逻辑的封装与委托”:
- 集合提供迭代器:具体聚合(如
ArrayBookShelf)通过createIterator()方法创建并返回对应的具体迭代器(如ArrayIterator),迭代器持有集合的引用; - 客户端使用迭代器遍历:客户端调用
createIterator()获取迭代器,通过迭代器的hasNext()和next()方法遍历集合,无需知道集合的内部结构; - 迭代器控制遍历过程:迭代器内部维护当前遍历位置,根据集合的存储结构(数组 / 链表)实现
hasNext()和next(),确保正确访问每个元素。
这种机制保证了:
- 客户端与集合的解耦(客户端→迭代器→集合,而非客户端→集合);
- 遍历逻辑的复用(同一迭代器可在多个客户端中使用);
- 集合类型的透明性(客户端无需区分数组、链表,统一用迭代器操作)。
七、迭代器模式的优缺点
优点:
- 统一遍历接口:用相同代码遍历不同集合,降低客户端学习和使用成本;
- 解耦集合与遍历:遍历逻辑与集合分离,集合结构变化(如数组改链表)只需修改对应迭代器,客户端无需变动;
- 保护集合封装性:集合无需暴露内部结构(如
getBooks()),仅通过迭代器提供访问,符合封装原则; - 支持多种遍历方式:同一集合可提供多个迭代器(正序、倒序、过滤),客户端按需选择,灵活扩展;
- 简化客户端代码:客户端无需编写复杂的遍历逻辑(如链表的
while循环),只需调用迭代器方法。
缺点:
- 类数量增加:每个集合需对应一个迭代器,集合类型增多时,类数量会膨胀(如 10 种集合对应 10 种迭代器);
- 遍历过程中修改集合有风险:若遍历中增删集合元素(如
remove()),可能导致迭代器指针异常(如漏遍历或重复遍历),需谨慎处理; - 迭代器功能有限:基础迭代器仅支持顺序遍历,复杂场景(如随机访问、并行遍历)需扩展接口,增加复杂度。
八、适用场景
迭代器模式适用于 “需要遍历多种集合,且希望隐藏集合内部结构” 的场景:
- 多集合类型遍历:系统中存在数组、链表、树等多种集合,需用统一方式遍历(如 Java 的
Collection框架,通过iterator()统一遍历); - 隐藏集合内部结构:集合的实现细节(如哈希表的桶结构)需对外屏蔽,仅暴露遍历接口(如数据库查询结果集的遍历);
- 支持多种遍历方式:同一集合需要正序、倒序、过滤等多种遍历方式(如文件系统的深度优先 / 广度优先遍历);
- 迭代器与集合分离:希望遍历逻辑可独立复用(如多个客户端共用同一迭代器遍历不同集合)。
典型案例:
- Java 的
java.util.Iterator(所有集合框架的基础迭代器接口); - C# 的
IEnumerable和IEnumerator(与 Java 迭代器模式一致); - 数据库的
ResultSet(封装查询结果的遍历,客户端无需知道数据存储结构); - 前端的
Array.prototype[Symbol.iterator](ES6 迭代器,统一数组、Map、Set 的遍历)。
九、迭代器模式 vs 访问者模式
两者都涉及集合遍历,但核心目标和关注点不同:
| 对比维度 | 迭代器模式 | 访问者模式 |
|---|---|---|
| 核心目标 | 提供统一的集合遍历方式,分离遍历与集合 | 定义对集合元素的操作,分离数据与操作 |
| 关注重点 | “如何遍历”(遍历逻辑) | “遍历做什么”(操作逻辑) |
| 角色协作 | 迭代器遍历集合,客户端通过迭代器获取元素 | 访问者遍历集合,对每个元素执行特定操作 |
| 适用场景 | 遍历不同集合,隐藏内部结构 | 对集合元素执行多种操作,且操作频繁变化 |
十、总结
迭代器模式的核心是 “遍历逻辑的统一与分离”,通过迭代器接口封装不同集合的遍历细节,让客户端用一致的方式遍历所有集合,既解耦了客户端与集合的依赖,又保护了集合的封装性。
它的关键是迭代器接口的设计(hasNext()和next()的标准化)和集合与迭代器的匹配(每个集合提供对应的迭代器)。实际开发中,当系统存在多种集合且需要统一遍历时,迭代器模式是简化代码、提升灵活性的最佳实践。
记住:迭代器模式是集合的 “统一遍历器”,让不同的集合 “用同一种语言说话”。
7.状态模式(State Pattern)
一、什么是状态模式?
状态模式是 ** 行为型模式中专注于 “对象状态与行为动态联动”** 的核心模式,其核心思想是:将对象的不同状态封装成独立的状态类,让对象在不同状态下表现出不同的行为,且状态之间的转换由状态类自身控制,使对象的状态变化与行为逻辑解耦。
简单说:“像电梯一样,在‘开门’状态下按‘运行’按钮无效,在‘关门’状态下按‘运行’才会启动,每个状态(开门 / 关门 / 运行 / 停止)对应不同的可执行行为,且状态之间的转换(如关门→运行)有明确规则”。
日常生活中,状态模式的例子随处可见:
- 订单状态(待支付→已支付→已发货→已签收):每个状态下能执行的操作不同(待支付时可取消,已发货时可退款);
- 交通信号灯(红→黄→绿):不同灯亮时车辆 / 行人的行为不同(红灯停、绿灯行);
- 手机状态(开机→锁屏→解锁):锁屏时无法操作 APP,解锁后才可使用。
二、为什么需要状态模式?(作用)
当系统中的对象存在多种状态,且不同状态下行为不同,状态之间有明确转换规则时,用if-else或switch-case硬编码状态逻辑会导致:
- 代码臃肿:每个行为方法(如电梯的 “开门”“运行”)都需要判断当前状态,大量条件判断掩盖核心逻辑;
- 扩展困难:新增状态(如电梯新增 “故障” 状态)需修改所有行为方法的条件判断,违反 “开闭原则”;
- 状态转换混乱:状态转换规则分散在各个条件判断中,难以追踪和维护(如 “待支付”→“已取消” 和 “待支付”→“已支付” 的转换逻辑);
- 职责不清:对象既负责自身业务逻辑,又负责状态管理,违背 “单一职责原则”。
状态模式的核心作用是:
- 消除条件判断:用多态替代
if-else,每个状态类封装对应状态的行为,代码更清晰; - 封装状态转换:状态之间的转换规则由状态类自身控制,集中管理,便于维护;
- 支持灵活扩展:新增状态只需新增状态类,无需修改原有代码,符合 “开闭原则”;
- 明确职责划分:对象(环境类)专注于业务,状态类专注于状态行为和转换,职责单一。
三、反例:用条件判断处理多状态的问题
假设我们要实现一个简单的电梯系统,电梯有 4 种状态:开门(OPEN)、关门(CLOSED)、运行(RUNNING)、停止(STOPPED),且不同状态下可执行的操作不同(如开门状态下不能运行,关门状态下可以运行)。
不使用状态模式的实现:
// 电梯类(包含所有状态和行为,问题核心)
class Elevator {
// 定义状态常量
public static final int OPEN = 1;
public static final int CLOSED = 2;
public static final int RUNNING = 3;
public static final int STOPPED = 4;
private int currentState; // 当前状态
public Elevator() {
currentState = CLOSED; // 初始状态:关门
}
// 开门操作(需判断当前状态是否允许)
public void open() {
if (currentState == CLOSED || currentState == STOPPED) {
System.out.println("电梯开门");
currentState = OPEN;
} else {
System.out.println("当前状态不允许开门(当前:" + getStateName() + ")");
}
}
// 关门操作
public void close() {
if (currentState == OPEN) {
System.out.println("电梯关门");
currentState = CLOSED;
} else {
System.out.println("当前状态不允许关门(当前:" + getStateName() + ")");
}
}
// 运行操作
public void run() {
if (currentState == CLOSED) {
System.out.println("电梯开始运行");
currentState = RUNNING;
} else {
System.out.println("当前状态不允许运行(当前:" + getStateName() + ")");
}
}
// 停止操作
public void stop() {
if (currentState == RUNNING) {
System.out.println("电梯停止");
currentState = STOPPED;
} else {
System.out.println("当前状态不允许停止(当前:" + getStateName() + ")");
}
}
// 辅助方法:获取状态名称
private String getStateName() {
switch (currentState) {
case OPEN: return "开门";
case CLOSED: return "关门";
case RUNNING: return "运行";
case STOPPED: return "停止";
default: return "未知";
}
}
}
// 客户端:操作电梯
public class Client {
public static void main(String[] args) {
Elevator elevator = new Elevator();
elevator.open(); // 允许:关门→开门
elevator.run(); // 不允许:开门状态不能运行
elevator.close(); // 允许:开门→关门
elevator.run(); // 允许:关门→运行
elevator.stop(); // 允许:运行→停止
elevator.open(); // 允许:停止→开门
// 输出:
// 电梯开门
// 当前状态不允许运行(当前:开门)
// 电梯关门
// 电梯开始运行
// 电梯停止
// 电梯开门
// 问题:新增“故障”状态需修改所有方法(open/close/run/stop)的if判断,违反开闭原则
// 问题:状态转换规则分散在各个方法中(如CLOSED→RUNNING在run()中),难以维护
// 问题:代码臃肿,每个方法都有冗长的条件判断,可读性差
}
}
问题分析:
- 违反开闭原则:新增状态(如 “故障”)时,需修改
Elevator中所有行为方法(open/close/run/stop)的if判断条件,原有代码面临被破坏的风险; - 条件判断臃肿:每个行为方法都包含大量
if-else,当状态增多(如 6 种状态),代码会变得冗长混乱,核心逻辑被条件判断掩盖; - 状态转换混乱:状态之间的转换规则(如 “关门→运行”“运行→停止”)分散在各个方法中,若需修改转换规则(如 “停止” 状态也能直接运行),需找到所有相关
if分支,维护成本高; - 职责过重:
Elevator类既负责电梯的业务操作(开门、运行等),又负责状态管理和转换,违背 “单一职责原则”。
四、正例:用状态模式解决问题
核心改进:将每种状态封装成独立的状态类,定义统一的状态接口;电梯类(环境)持有当前状态对象,将行为委托给当前状态;状态转换由状态类自身控制,消除条件判断,集中管理转换规则。
状态模式的实现:
// 1. 抽象状态(State):定义所有状态的行为接口
interface ElevatorState {
void open(Elevator elevator); // 开门行为
void close(Elevator elevator); // 关门行为
void run(Elevator elevator); // 运行行为
void stop(Elevator elevator); // 停止行为
}
// 2. 具体状态1:开门状态(OPEN)
class OpenState implements ElevatorState {
@Override
public void open(Elevator elevator) {
System.out.println("当前已是开门状态,无需再开门");
}
@Override
public void close(Elevator elevator) {
System.out.println("电梯关门");
// 状态转换:开门→关门(将电梯的当前状态改为关门状态)
elevator.setState(new ClosedState());
}
@Override
public void run(Elevator elevator) {
System.out.println("开门状态下不能运行");
}
@Override
public void stop(Elevator elevator) {
System.out.println("开门状态下无需停止");
}
}
// 3. 具体状态2:关门状态(CLOSED)
class ClosedState implements ElevatorState {
@Override
public void open(Elevator elevator) {
System.out.println("电梯开门");
// 状态转换:关门→开门
elevator.setState(new OpenState());
}
@Override
public void close(Elevator elevator) {
System.out.println("当前已是关门状态,无需再关门");
}
@Override
public void run(Elevator elevator) {
System.out.println("电梯开始运行");
// 状态转换:关门→运行
elevator.setState(new RunningState());
}
@Override
public void stop(Elevator elevator) {
System.out.println("关门状态下无需停止(未运行)");
}
}
// 4. 具体状态3:运行状态(RUNNING)
class RunningState implements ElevatorState {
@Override
public void open(Elevator elevator) {
System.out.println("运行状态下不能开门");
}
@Override
public void close(Elevator elevator) {
System.out.println("运行状态下已是关门状态");
}
@Override
public void run(Elevator elevator) {
System.out.println("当前已是运行状态,无需再运行");
}
@Override
public void stop(Elevator elevator) {
System.out.println("电梯停止");
// 状态转换:运行→停止
elevator.setState(new StoppedState());
}
}
// 5. 具体状态4:停止状态(STOPPED)
class StoppedState implements ElevatorState {
@Override
public void open(Elevator elevator) {
System.out.println("电梯开门");
// 状态转换:停止→开门
elevator.setState(new OpenState());
}
@Override
public void close(Elevator elevator) {
System.out.println("停止状态下已是关门状态");
}
@Override
public void run(Elevator elevator) {
System.out.println("电梯开始运行");
// 状态转换:停止→运行
elevator.setState(new RunningState());
}
@Override
public void stop(Elevator elevator) {
System.out.println("当前已是停止状态,无需再停止");
}
}
// 6. 环境类(Context):电梯,持有当前状态并委托行为
class Elevator {
private ElevatorState currentState;
public Elevator() {
// 初始状态:关门
this.currentState = new ClosedState();
}
// 设置当前状态(供状态类调用,实现状态转换)
public void setState(ElevatorState state) {
this.currentState = state;
}
// 开门:委托给当前状态
public void open() {
currentState.open(this);
}
// 关门:委托给当前状态
public void close() {
currentState.close(this);
}
// 运行:委托给当前状态
public void run() {
currentState.run(this);
}
// 停止:委托给当前状态
public void stop() {
currentState.stop(this);
}
}
// 7. 客户端:操作电梯,无需关心状态逻辑
public class Client {
public static void main(String[] args) {
Elevator elevator = new Elevator();
elevator.open(); // 关门状态→开门
elevator.run(); // 开门状态不能运行
elevator.close(); // 开门→关门
elevator.run(); // 关门→运行
elevator.stop(); // 运行→停止
elevator.open(); // 停止→开门
// 输出与反例一致,但代码结构更清晰
// 新增“故障”状态:只需新增FaultState类实现ElevatorState,无需修改其他类
// elevator.setState(new FaultState());
// elevator.open(); // 故障状态下的行为
}
}
改进效果:
- 消除条件判断:
Elevator类的行为方法(open/close)不再有if-else,而是委托给当前状态对象,代码简洁清晰,核心逻辑突出; - 封装状态转换:状态之间的转换规则(如
ClosedState→RunningState)集中在具体状态类中,便于追踪和修改(如修改 “停止→运行” 的规则,只需改StoppedState的run方法); - 符合开闭原则:新增状态(如 “故障”)时,只需新增
FaultState实现ElevatorState,Elevator和其他状态类无需修改,扩展安全且灵活; - 职责单一:
Elevator专注于提供电梯操作接口,状态类专注于对应状态的行为和转换,各司其职,符合 “单一职责原则”; - 状态行为清晰:每个状态的允许行为和禁止行为在对应状态类中一目了然(如
OpenState的run方法直接提示 “不能运行”),可读性和可维护性大幅提升。
五、状态模式的核心结构
状态模式通过 “状态封装 + 委托执行” 实现状态与行为的联动,包含 3 个核心角色:
- 抽象状态(State):
- 定义所有具体状态的行为接口,声明对象在不同状态下可执行的操作(如
ElevatorState的open/close/run/stop); - 接口方法通常需要传入环境类(
Elevator)的引用,以便状态类在转换时修改环境的当前状态。
- 定义所有具体状态的行为接口,声明对象在不同状态下可执行的操作(如
- 具体状态(Concrete State):
- 实现
State接口,封装对应状态下的行为逻辑(如OpenState中 “开门状态不能运行”); - 负责状态转换控制:在行为执行后,根据业务规则将环境的当前状态切换到其他状态(如
ClosedState的run方法将电梯状态改为RunningState); - 每个具体状态类对应对象的一种状态,确保状态行为的独立性。
- 实现
- 环境类(Context):
- 是拥有状态的对象(如
Elevator),持有State接口的引用(当前状态); - 提供对外的业务接口(如
open/close),但自身不实现行为,而是将行为委托给当前状态对象; - 提供
setState方法,允许状态类修改其当前状态,实现状态转换。
- 是拥有状态的对象(如
六、状态模式的工作原理
状态模式的核心是 “状态驱动行为,行为触发转换”:
- 环境持有当前状态:环境类(如电梯)初始化时设置初始状态(如
ClosedState),并通过currentState引用当前状态; - 行为委托给状态:客户端调用环境的业务方法(如
elevator.open())时,环境将调用转发给当前状态对象(currentState.open(this)); - 状态处理行为并转换:具体状态类(如
ClosedState)处理行为(如执行 “开门” 逻辑),并根据规则通过setState方法切换环境的当前状态(如切换到OpenState); - 新状态响应后续行为:环境的当前状态更新后,后续行为将由新的状态对象处理(如下次调用
close()时,由OpenState处理)。
这种机制保证了:
- 状态与行为的绑定(不同状态对应不同行为);
- 状态转换的集中管理(转换规则在状态类中);
- 环境与状态的解耦(环境仅依赖
State接口)。
七、状态模式的优缺点
优点:
- 消除条件判断:用多态替代
if-else,代码更简洁,可读性和可维护性提升; - 封装状态转换:状态转换规则集中在状态类中,便于理解和修改;
- 符合开闭原则:新增状态只需新增状态类,无需修改环境和其他状态,扩展灵活;
- 职责单一:环境类和状态类各司其职,环境管业务接口,状态管行为和转换;
- 状态行为清晰:每个状态的行为在对应类中一目了然,便于调试和扩展。
缺点:
- 类数量增加:每种状态对应一个类,状态较多时(如 10 种)会导致类数量膨胀,增加系统复杂度;
- 状态转换依赖环境:状态类需要持有环境引用才能调用
setState,可能导致状态与环境的耦合度升高; - 状态逻辑分散:若状态转换规则复杂(如多条件触发转换),逻辑会分散在多个状态类中,难以全局把握;
- 初始化成本高:若状态初始化需要大量资源(如加载配置),多个状态类的初始化可能影响性能。
八、适用场景
状态模式适用于 “对象存在多种状态,不同状态下行为不同,且状态间有明确转换规则” 的场景:
- 状态驱动的行为:如电梯(开门 / 关门 / 运行 / 停止)、订单(待支付 / 已支付 / 已取消)、交通信号灯(红 / 黄 / 绿);
- 替代复杂条件判断:当代码中出现超过 3 个与状态相关的
if-else或switch-case,且判断逻辑重复时,适合用状态模式重构; - 状态转换规则明确:状态之间的转换有清晰的业务规则(如 “待支付” 超时→“已取消”,支付成功→“已支付”);
- 状态行为多变:不同状态下的行为差异较大,且可能频繁调整(如会员等级(普通 / 黄金 / 钻石)对应的折扣行为)。
典型案例:
- 订单系统的状态流转(待支付→已支付→已发货→已完成);
- 游戏角色状态(正常 / 受伤 / 死亡)对应的行为(移动 / 减速 / 无法移动);
- 工作流引擎(如请假审批流程:部门审批→总经理审批→完成);
- 设备状态管理(如打印机:空闲→打印中→卡纸→空闲)。
九、状态模式 vs 策略模式
两者结构相似(都通过接口和多态实现行为变化),但核心目标和行为驱动方式不同:
| 对比维度 | 状态模式 | 策略模式 |
|---|---|---|
| 核心目标 | 封装对象的状态及状态对应的行为,解决 “状态驱动行为” 问题 | 封装可互换的算法,解决 “算法灵活选择” 问题 |
| 行为触发方式 | 行为由对象内部状态驱动(状态变化自动切换行为) | 行为由客户端主动选择策略(外部驱动) |
| 状态 / 策略关系 | 状态之间可能存在依赖和转换(如关门→运行) | 策略之间完全独立,无依赖关系(如支付宝 / 微信支付) |
| 环境与实现的耦合 | 状态类通常需要环境引用以实现状态转换(耦合稍高) | 策略类无需环境引用(耦合低) |
| 适用场景 | 状态变化导致行为变化(电梯、订单) | 算法选择导致行为变化(支付方式、排序) |
十、总结
状态模式的核心是 “状态封装,行为联动”,通过将对象的每种状态封装成独立类,让对象在不同状态下自动表现出不同行为,同时集中管理状态转换规则,解决了多状态场景下条件判断臃肿、扩展困难的问题。
它的关键是抽象状态接口的设计(统一行为)和状态类对转换规则的控制(集中管理)。实际开发中,当对象的行为依赖于其状态且状态频繁变化时,状态模式是提升代码清晰度和可维护性的最佳实践。
记住:状态模式让对象的 “状态” 自己 “做主”,在什么状态下该做什么,由状态自己定义。
8 命令模式(Command Pattern)
一、什么是命令模式?
命令模式是 行为型模式中专注于 “请求封装与解耦”的核心模式,其核心思想是:将一个请求(如 “打开文件”“关闭窗口”)封装成一个独立的命令对象,通过命令对象来参数化对请求的调用、排队、记录日志或撤销,使请求的发送者(如按钮)与接收者(如文件系统)完全解耦。
简单说:“像餐厅点餐一样,顾客(发送者)下单(命令)给服务员(调用者),服务员将订单交给厨师(接收者)。顾客不需要知道厨师是谁、怎么做菜,只需通过订单传递需求,且订单可记录、可取消(如退菜)”。
日常生活中,命令模式的例子随处可见:
- 遥控器按钮(调用者):每个按钮对应一个命令(开空调、关灯光),按钮无需知道空调 / 灯光的具体实现;
- 快捷键操作(如 Ctrl+S 保存):快捷键是发送者,“保存” 命令封装了对文件系统的操作,快捷键无需知道文件如何保存;
- 任务调度系统:待执行的任务被封装成命令,放入队列中按顺序执行,调度器无需知道任务的具体逻辑。
二、为什么需要命令模式?(作用)
当系统中存在 “请求的发送者(如按钮)需要调用接收者(如功能模块)的功能,但两者直接耦合会导致扩展困难” 的场景时,直接在发送者中硬编码接收者的调用逻辑会导致:
- 发送者与接收者强耦合:发送者必须知道接收者的具体类型和方法(如按钮直接调用
AirConditioner.turnOn()),新增接收者需修改发送者代码; - 无法支持请求排队 / 延迟执行:若需要将多个请求按顺序执行(如批量操作)或定时执行,硬编码难以实现;
- 缺乏请求记录与撤销能力:如文档编辑的 “撤销(Ctrl+Z)” 功能,硬编码无法追踪历史请求并反向执行;
- 功能扩展困难:新增功能(如遥控器新增 “调节温度” 按钮)需修改发送者的逻辑,违反 “开闭原则”。
命令模式的核心作用是:
- 解耦发送者与接收者:发送者只需调用命令的
execute方法,无需知道接收者是谁、如何实现; - 支持请求的灵活管理:命令可被存储(日志)、排队(任务队列)、延迟执行(定时任务);
- 实现撤销与重做:通过命令的
undo方法反向执行操作,支持多级撤销; - 便于扩展新命令:新增命令只需实现命令接口,无需修改发送者和接收者,符合 “开闭原则”。
三、反例:发送者与接收者直接耦合的问题
假设我们要实现一个简单的遥控器,控制两种家电:电视(TV)和空调(AirConditioner),遥控器有两个按钮,分别对应 “开电视” 和 “开空调”。
不使用命令模式的实现:
// 1. 接收者1:电视
class TV {
public void turnOn() {
System.out.println("电视打开了");
}
public void turnOff() {
System.out.println("电视关闭了");
}
}
// 2. 接收者2:空调
class AirConditioner {
public void turnOn() {
System.out.println("空调打开了");
}
public void turnOff() {
System.out.println("空调关闭了");
}
}
// 3. 发送者:遥控器(直接依赖具体接收者,问题核心)
class RemoteControl {
// 硬编码依赖具体家电,新增家电需修改此处
private TV tv;
private AirConditioner ac;
public RemoteControl(TV tv, AirConditioner ac) {
this.tv = tv;
this.ac = ac;
}
// 按钮1:开电视(直接调用TV的方法,耦合)
public void pressButton1() {
tv.turnOn();
}
// 按钮2:开空调(直接调用AirConditioner的方法,耦合)
public void pressButton2() {
ac.turnOn();
}
// 若新增“关电视”按钮,需新增方法:
// public void pressButton3() { tv.turnOff(); }
}
// 客户端:使用遥控器
public class Client {
public static void main(String[] args) {
TV tv = new TV();
AirConditioner ac = new AirConditioner();
RemoteControl remote = new RemoteControl(tv, ac);
remote.pressButton1(); // 开电视
remote.pressButton2(); // 开空调
// 输出:
// 电视打开了
// 空调打开了
// 问题:新增“灯”需要修改RemoteControl,添加Light成员和对应按钮方法
// 问题:无法实现“撤销”(如按错按钮关闭电视)
// 问题:无法将多个操作排队执行(如同时开电视和空调)
}
}
问题分析:
- 强耦合:
RemoteControl直接依赖TV和AirConditioner的具体实现,若家电的方法名变更(如turnOn改为powerOn),遥控器代码需同步修改; - 扩展困难:新增家电(如
Light)或功能(如 “关电视”)时,必须修改RemoteControl,添加新的成员变量和按钮方法,违反 “开闭原则”; - 缺乏灵活性:无法实现请求的排队(如先开空调、10 秒后开电视)、日志记录(记录谁在何时开了电视)或撤销(按错按钮后关闭电视);
- 职责混乱:遥控器既负责接收用户输入(按钮按压),又负责直接调用家电的方法,违背 “单一职责原则”。
四、正例:用命令模式解决问题
核心改进:定义 “命令接口” 封装请求(execute执行,undo撤销),每个具体请求(开电视、关空调)对应一个具体命令类,遥控器(调用者)持有命令对象并触发执行,实现发送者与接收者的解耦。
命令模式的实现:
// 1. 命令接口(Command):定义执行和撤销方法
interface Command {
void execute(); // 执行命令
void undo(); // 撤销命令(可选,按需实现)
}
// 2. 具体命令1:开电视
class TVOnCommand implements Command {
private TV tv; // 持有接收者引用
public TVOnCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.turnOn(); // 调用接收者的方法
}
@Override
public void undo() {
tv.turnOff(); // 撤销:执行相反操作
}
}
// 3. 具体命令2:关电视
class TVOffCommand implements Command {
private TV tv;
public TVOffCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.turnOff();
}
@Override
public void undo() {
tv.turnOn(); // 撤销:开电视
}
}
// 4. 具体命令3:开空调
class AConCommand implements Command {
private AirConditioner ac;
public AConCommand(AirConditioner ac) {
this.ac = ac;
}
@Override
public void execute() {
ac.turnOn();
}
@Override
public void undo() {
ac.turnOff();
}
}
// 5. 调用者(Invoker):遥控器,持有命令对象
class RemoteControl {
private Command[] onCommands; // 存储“开”命令
private Command[] offCommands; // 存储“关”命令
private Command lastCommand; // 记录最后执行的命令(用于撤销)
public RemoteControl(int buttonCount) {
onCommands = new Command[buttonCount];
offCommands = new Command[buttonCount];
// 初始化空命令(避免空指针,默认不做任何操作)
Command noCommand = new NoCommand();
for (int i = 0; i < buttonCount; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
lastCommand = noCommand;
}
// 为按钮设置命令
public void setCommand(int index, Command onCmd, Command offCmd) {
onCommands[index] = onCmd;
offCommands[index] = offCmd;
}
// 按下“开”按钮
public void pressOnButton(int index) {
onCommands[index].execute();
lastCommand = onCommands[index]; // 记录最后执行的命令
}
// 按下“关”按钮
public void pressOffButton(int index) {
offCommands[index].execute();
lastCommand = offCommands[index];
}
// 按下撤销按钮
public void pressUndoButton() {
lastCommand.undo();
}
}
// 6. 空命令(NoCommand):避免空指针,作为默认命令
class NoCommand implements Command {
@Override
public void execute() {} // 空实现
@Override
public void undo() {} // 空实现
}
// 7. 接收者(不变):电视和空调
class TV {
public void turnOn() { System.out.println("电视打开了"); }
public void turnOff() { System.out.println("电视关闭了"); }
}
class AirConditioner {
public void turnOn() { System.out.println("空调打开了"); }
public void turnOff() { System.out.println("空调关闭了"); }
}
// 8. 客户端:配置命令与遥控器
public class Client {
public static void main(String[] args) {
// 创建接收者
TV tv = new TV();
AirConditioner ac = new AirConditioner();
// 创建具体命令
Command tvOn = new TVOnCommand(tv);
Command tvOff = new TVOffCommand(tv);
Command acOn = new AConCommand(ac);
Command acOff = new AConCommand(ac) { // 匿名类实现关空调
@Override
public void execute() { ac.turnOff(); }
@Override
public void undo() { ac.turnOn(); }
};
// 创建遥控器(2个按钮)
RemoteControl remote = new RemoteControl(2);
// 为按钮0设置电视命令
remote.setCommand(0, tvOn, tvOff);
// 为按钮1设置空调命令
remote.setCommand(1, acOn, acOff);
// 操作遥控器
System.out.println("=== 按下按钮0开电视 ===");
remote.pressOnButton(0); // 电视打开了
System.out.println("\n=== 按下按钮1开空调 ===");
remote.pressOnButton(1); // 空调打开了
System.out.println("\n=== 按下撤销按钮 ===");
remote.pressUndoButton(); // 撤销最后一个操作(关空调)
System.out.println("\n=== 按下按钮0关电视 ===");
remote.pressOffButton(0); // 电视关闭了
System.out.println("\n=== 按下撤销按钮 ===");
remote.pressUndoButton(); // 撤销(开电视)
// 输出:
// === 按下按钮0开电视 ===
// 电视打开了
//
// === 按下按钮1开空调 ===
// 空调打开了
//
// === 按下撤销按钮 ===
// 空调关闭了
//
// === 按下按钮0关电视 ===
// 电视关闭了
//
// === 按下撤销按钮 ===
// 电视打开了
}
}
改进效果:
- 解耦发送者与接收者:
RemoteControl(发送者)仅依赖Command接口,无需知道TV或AirConditioner的存在,新增家电(如Light)只需新增命令类,遥控器代码无需修改; - 支持撤销操作:通过
lastCommand记录历史命令,调用undo方法反向执行(如开电视→撤销→关电视),轻松实现多级撤销(扩展lastCommand为历史列表即可); - 灵活扩展新命令:新增功能(如 “调节空调温度”)只需实现
Command接口(ACAdjustCommand),通过setCommand绑定到遥控器按钮,符合 “开闭原则”; - 支持请求管理:命令对象可被存储在队列中(如
List<Command>),实现批量执行(如 “回家模式”:同时开电视、开空调、开灯)或定时执行(延迟调用execute); - 避免空指针:通过
NoCommand作为默认命令,解决未设置命令时的空指针问题,代码更健壮。
五、命令模式的核心结构
命令模式通过 “命令封装请求,调用者触发执行” 实现解耦,包含 5 个核心角色:
- 命令接口(Command):
- 定义命令的统一接口,至少包含一个
execute()方法用于执行请求; - 可选扩展
undo()方法用于撤销请求(反向执行execute的操作); - 是连接调用者与接收者的桥梁,确保调用者无需知道具体命令细节。
- 定义命令的统一接口,至少包含一个
- 具体命令(Concrete Command):
- 实现
Command接口,封装具体的请求逻辑; - 持有接收者(Receiver)的引用,在
execute()中调用接收者的具体方法(如TVOnCommand调用TV.turnOn()); - 若支持撤销,
undo()方法需实现与execute()相反的操作(如TVOnCommand的undo调用TV.turnOff())。
- 实现
- 接收者(Receiver):
- 是实际执行请求的对象(如
TV、AirConditioner),包含具体的业务逻辑(turnOn、turnOff); - 与命令对象耦合,但与调用者完全解耦(接收者不知道调用者的存在)。
- 是实际执行请求的对象(如
- 调用者(Invoker):
- 是触发命令执行的对象(如
RemoteControl),持有命令对象的引用; - 不直接调用接收者的方法,而是通过调用命令的
execute()间接执行请求; - 负责管理命令(如存储命令队列、记录历史命令用于撤销)。
- 是触发命令执行的对象(如
- 客户端(Client):
- 负责创建具体命令对象,并将命令与接收者绑定(如
new TVOnCommand(tv)); - 将命令设置到调用者中(如
remote.setCommand(0, tvOn, tvOff)),完成整个调用链的组装。
- 负责创建具体命令对象,并将命令与接收者绑定(如
六、命令模式的工作原理
命令模式的核心是 “请求的封装与传递”,流程如下:
- 客户端组装命令:客户端创建接收者(如
TV)和具体命令(如TVOnCommand),将接收者传入命令(命令持有接收者引用); - 调用者绑定命令:调用者(如遥控器)通过
setCommand方法接收命令,存储在内部(如onCommands数组); - 触发命令执行:用户操作调用者(如按遥控器按钮),调用者调用命令的
execute()方法; - 命令调用接收者:具体命令的
execute()方法调用接收者的业务方法(如tv.turnOn()),完成请求; - 撤销(可选):调用者通过
undo()方法触发命令的反向操作(如lastCommand.undo()),实现撤销。
这种机制保证了:
- 调用者与接收者的完全解耦(通过命令接口隔离);
- 命令的可管理性(可存储、排队、记录日志);
- 功能的可扩展性(新增命令不影响现有代码)。
七、命令模式的优缺点
优点:
- 解耦调用者与接收者:两者通过命令接口交互,互不依赖具体实现,降低耦合度;
- 支持撤销与重做:通过
undo和redo方法(扩展命令接口)可反向执行操作,适合编辑类软件; - 便于请求管理:命令可被存储在队列、栈中,支持批量执行、延迟执行、事务回滚(如数据库事务);
- 符合开闭原则:新增命令只需实现接口,无需修改调用者和接收者,扩展灵活;
- 集中控制请求:所有请求通过命令执行,便于日志记录(谁在何时执行了什么命令)和权限控制。
缺点:
- 类数量增加:每个具体请求对应一个命令类,功能复杂时会导致类数量膨胀(如 10 个功能对应 10 个命令类);
- 简单场景冗余:对于无需撤销、排队的简单请求(如单次调用),使用命令模式会增加代码复杂度;
- 命令与接收者耦合:具体命令需持有接收者引用,若接收者接口变更,所有依赖它的命令需同步修改。
八、适用场景
命令模式适用于 “需要解耦请求发送者与接收者,或需要对请求进行管理(排队、撤销、日志)” 的场景:
- 请求的发送者与接收者需解耦:如 GUI 按钮(发送者)与业务逻辑(接收者),按钮无需知道业务逻辑如何实现;
- 需要支持撤销 / 重做:如文本编辑器(撤销输入)、绘图软件(撤销上一步绘制)、数据库事务(回滚);
- 请求需要排队或批量执行:如任务调度系统(按顺序执行多个任务)、批处理操作(同时删除多个文件);
- 需要日志记录与回放:如命令日志(记录所有操作,故障时回放恢复状态)、游戏录像(记录玩家操作并回放);
- 需要抽象回调行为:如事件驱动系统(点击事件封装成命令,由事件处理器执行)。
典型案例:
- Java 的
Runnable接口(将任务封装成命令,Thread作为调用者执行); - Spring 的
JmsTemplate(消息发送封装成命令,支持异步执行); - 遥控器、游戏手柄的按键映射(每个按键绑定一个命令);
- 事务管理(
TransactionCommand封装事务操作,支持提交 / 回滚)。
九、命令模式 vs 代理模式
两者都通过 “中间对象” 间接调用目标,但核心目标不同:
| 对比维度 | 命令模式 | 代理模式 |
|---|---|---|
| 核心目标 | 封装请求,解耦发送者与接收者,支持请求管理 | 控制对目标对象的访问(如权限、延迟加载) |
| 中间对象角色 | 命令对象封装请求逻辑,主动调用接收者方法 | 代理对象拦截访问,被动转发调用给目标 |
| 交互方式 | 调用者→命令→接收者(主动触发执行) | 客户端→代理→目标(代理被动转发) |
| 侧重点 | 关注 “请求的执行、撤销、排队” | 关注 “访问的控制、增强”(如日志、缓存) |
十、总结
命令模式的核心是 “请求封装,解耦交互”,通过将每个请求封装成独立的命令对象,实现了发送者与接收者的完全解耦,同时支持请求的排队、撤销、日志等高级功能,解决了硬编码耦合导致的扩展困难问题。
它的关键是命令接口的设计(execute和undo方法)和调用者对命令的管理(存储、触发、撤销)。实际开发中,当需要灵活管理请求(尤其是撤销、排队场景)或解耦发送者与接收者时,命令模式是提升系统灵活性和可维护性的最佳实践。
记住:命令模式让每个 “请求” 成为一个 “对象”,让请求的 “发送” 与 “执行” 各安其位,互不干扰。
9 中介者模式(Mediator Pattern)
一、什么是中介者模式?
中介者模式是 行为型模式中专注于 “化解对象间复杂交互”的核心模式,其核心思想是:引入一个 “中介者” 对象,让原本相互耦合的多个对象(同事)通过中介者间接通信,从而减少对象间的直接依赖,将多对多的复杂关系简化为一对多。
简单说:“像机场塔台一样,多架飞机(同事)之间不直接通信,而是通过塔台(中介者)协调起飞、降落,塔台统一处理所有飞机的请求,避免飞机之间的混乱交互”。
日常生活中,中介者模式的例子随处可见:
- 房屋中介:房东和租客(同事)不直接沟通,通过中介(中介者)协调价格、签约,减少双方的直接依赖;
- 聊天室:多个用户(同事)发送消息时,消息先发给聊天室服务器(中介者),再由服务器转发给其他用户,用户之间无需知道彼此的存在;
- 交通信号灯:路口的车辆和行人(同事)通过信号灯(中介者)判断是否通行,无需直接互动。
二、为什么需要中介者模式?(作用)
当系统中存在多个对象相互依赖、直接通信(多对多关系) 时,对象间的强耦合会导致:
- 耦合度极高:每个对象都需持有其他多个对象的引用(如聊天室中用户 A 要给 B、C 发消息,需同时引用 B 和 C),新增对象时需修改所有相关对象的代码;
- 维护困难:对象间的交互逻辑分散在各个对象中(如 A→B 的消息处理在 A 中,B→A 的处理在 B 中),修改交互规则需改动多个类;
- 扩展性差:删除一个对象时,需从所有依赖它的对象中移除引用,否则会导致错误;
- 逻辑混乱:多对多的关系形成 “网状结构”,难以追踪对象间的交互流程(如 10 个对象相互通信,会形成 45 对直接关系)。
中介者模式的核心作用是:
- 降低耦合度:将对象间的直接通信改为通过中介者间接通信,消除对象间的直接依赖;
- 集中交互逻辑:所有对象的交互规则集中在中介者中,便于统一管理和修改;
- 简化对象职责:对象(同事)只需关注自身业务,无需关心其他对象的存在和交互方式;
- 提高扩展性:新增对象(同事)只需注册到中介者,无需修改其他对象,符合 “开闭原则”。
三、反例:对象间直接通信的网状耦合问题
假设我们要实现一个简单的聊天室,有 3 个用户(UserA、UserB、UserC),用户之间可以互相发送消息。
不使用中介者模式的实现:
// 1. 用户类(同事):直接持有其他用户的引用,问题核心
class User {
private String name;
// 持有所有其他用户的引用(多对多耦合)
private List<User> otherUsers;
public User(String name) {
this.name = name;
this.otherUsers = new ArrayList<>();
}
// 添加其他用户(需手动维护引用关系)
public void addUser(User user) {
otherUsers.add(user);
}
// 发送消息:直接调用其他用户的接收方法(强耦合)
public void sendMessage(String message) {
System.out.println(name + "发送消息:" + message);
// 遍历所有其他用户,通知接收
for (User user : otherUsers) {
user.receiveMessage(name, message);
}
}
// 接收消息
public void receiveMessage(String sender, String message) {
System.out.println(name + "收到" + sender + "的消息:" + message);
}
}
// 客户端:创建用户并维护引用关系
public class Client {
public static void main(String[] args) {
// 创建3个用户
User a = new User("A");
User b = new User("B");
User c = new User("C");
// 手动添加相互引用(每个用户必须知道其他所有用户)
a.addUser(b);
a.addUser(c);
b.addUser(a);
b.addUser(c);
c.addUser(a);
c.addUser(b);
// 发送消息
System.out.println("=== A发送消息 ===");
a.sendMessage("大家好!");
System.out.println("\n=== B发送消息 ===");
b.sendMessage("Hi A!");
// 输出:
// === A发送消息 ===
// A发送消息:大家好!
// B收到A的消息:大家好!
// C收到A的消息:大家好!
//
// === B发送消息 ===
// B发送消息:Hi A!
// A收到B的消息:Hi A!
// C收到B的消息:Hi A!
// 问题:新增用户D时,需修改A、B、C的addUser调用,违反开闭原则
// 问题:用户C退出时,需从A、B的otherUsers中移除,否则发送消息时会报错
// 问题:交互逻辑分散在每个User的sendMessage中,修改转发规则(如只发给在线用户)需改所有User
}
}
问题分析:
- 网状耦合严重:3 个用户形成 6 对直接引用关系(A→B、A→C、B→A、B→C、C→A、C→B),若有 10 个用户,会形成 90 对关系,维护成本随用户数量呈指数增长;
- 扩展困难:新增用户 D 时,必须修改现有所有用户(A、B、C)的
addUser方法,将 D 加入它们的引用列表,违反 “开闭原则”; - 交互逻辑分散:消息转发规则(如 “发给所有用户”)嵌入在每个
User的sendMessage中,若需修改规则(如 “只发给好友”),需修改所有User类的代码; - 对象职责过重:
User不仅要处理自身的发送 / 接收逻辑,还要维护与其他用户的引用关系,违背 “单一职责原则”。
四、正例:用中介者模式解决问题
核心改进:引入 “聊天室中介者”,所有用户(同事)只与中介者通信,不直接引用其他用户;中介者持有所有用户的引用,负责消息的转发和交互协调,将多对多关系简化为一对多。
中介者模式的实现:
import java.util.ArrayList;
import java.util.List;
// 1. 抽象中介者(Mediator):定义同事与中介者的通信接口
interface ChatMediator {
void registerUser(User user); // 注册用户
void sendMessage(String message, User sender); // 转发消息
}
// 2. 具体中介者:聊天室中介者,实现通信逻辑
class ChatRoom implements ChatMediator {
private List<User> users; // 持有所有用户的引用
public ChatRoom() {
this.users = new ArrayList<>();
}
@Override
public void registerUser(User user) {
users.add(user);
// 让用户知道自己的中介者(可选,便于用户主动发送消息)
user.setMediator(this);
}
@Override
public void sendMessage(String message, User sender) {
// 转发消息给除发送者外的所有用户(集中处理交互规则)
for (User user : users) {
if (user != sender) { // 不发给自己
user.receiveMessage(sender.getName(), message);
}
}
}
}
// 3. 抽象同事类(Colleague):定义与中介者的交互接口
abstract class User {
protected String name;
protected ChatMediator mediator; // 持有中介者引用(仅与中介者通信)
public User(String name) {
this.name = name;
}
public void setMediator(ChatMediator mediator) {
this.mediator = mediator;
}
public String getName() {
return name;
}
// 发送消息:委托给中介者
public abstract void send(String message);
// 接收消息
public abstract void receiveMessage(String sender, String message);
}
// 4. 具体同事类:普通用户
class CommonUser extends User {
public CommonUser(String name) {
super(name);
}
@Override
public void send(String message) {
System.out.println(name + "发送消息:" + message);
// 不直接调用其他用户,通过中介者转发
mediator.sendMessage(message, this);
}
@Override
public void receiveMessage(String sender, String message) {
System.out.println(name + "收到" + sender + "的消息:" + message);
}
}
// 客户端:通过中介者管理用户交互
public class Client {
public static void main(String[] args) {
// 创建中介者(聊天室)
ChatMediator chatRoom = new ChatRoom();
// 创建用户并注册到中介者(无需用户间相互引用)
User a = new CommonUser("A");
User b = new CommonUser("B");
User c = new CommonUser("C");
chatRoom.registerUser(a);
chatRoom.registerUser(b);
chatRoom.registerUser(c);
// 发送消息(用户只需调用自己的send方法,通过中介者转发)
System.out.println("=== A发送消息 ===");
a.send("大家好!");
System.out.println("\n=== B发送消息 ===");
b.send("Hi A!");
// 输出与反例一致,但结构更清晰
// 新增用户D:只需注册到中介者,无需修改其他用户
System.out.println("\n=== 新增用户D ===");
User d = new CommonUser("D");
chatRoom.registerUser(d);
d.send("我是新来的!");
// 输出:
// D发送消息:我是新来的!
// A收到D的消息:我是新来的!
// B收到D的消息:我是新来的!
// C收到D的消息:我是新来的!
}
}
改进效果:
- 解耦同事关系:用户(同事)之间不再相互引用,仅通过
ChatRoom(中介者)通信,3 个用户的关系从 “网状” 简化为 “星状”(所有用户指向中介者),耦合度大幅降低; - 集中交互逻辑:消息转发规则(如 “发给除自己外的所有用户”)集中在
ChatRoom中,修改规则(如 “只发给在线用户”)只需修改ChatRoom,无需改动User类; - 符合开闭原则:新增用户 D 时,只需创建
CommonUser并注册到ChatRoom,现有用户 A、B、C 的代码无需任何修改,扩展安全且灵活; - 简化同事职责:
User类只需关注 “发送自己的消息” 和 “接收消息”,无需维护与其他用户的关系,职责单一,代码清晰; - 便于管理交互:中介者可统一记录消息日志、验证权限(如禁止匿名用户发言),所有交互通过中介者,便于监控和控制。
五、中介者模式的核心结构
中介者模式通过 “中介者协调,同事间接通信” 实现解耦,包含 4 个核心角色:
- 抽象中介者(Mediator):
- 定义中介者与同事的通信接口,通常包含
register(注册同事)和send(转发消息)等方法; - 是同事对象与中介者交互的契约,确保同事无需知道具体中介者的实现。
- 定义中介者与同事的通信接口,通常包含
- 具体中介者(Concrete Mediator):
- 实现
Mediator接口,持有所有同事对象的引用(如ChatRoom的users列表); - 实现交互协调逻辑:接收同事发送的消息,根据规则转发给其他同事(如 “排除发送者”“按权限过滤”);
- 是整个模式的核心,集中管理所有同事的交互规则。
- 实现
- 抽象同事类(Colleague):
- 定义同事的基本行为(如发送、接收消息),并持有
Mediator接口的引用; - 声明同事与中介者的交互方法(如
send方法委托给中介者),确保所有同事以统一方式与中介者通信。
- 定义同事的基本行为(如发送、接收消息),并持有
- 具体同事类(Concrete Colleague):
- 继承
Colleague类,是实际参与交互的对象(如CommonUser); - 实现
send方法时,不直接调用其他同事,而是通过持有的中介者引用转发消息(mediator.sendMessage(...)); - 实现
receiveMessage方法,处理收到的消息,专注于自身业务逻辑。
- 继承
六、中介者模式的工作原理
中介者模式的核心是 “中介者作为交互枢纽”,流程如下:
- 同事注册到中介者:所有同事对象创建后,通过中介者的
register方法注册,中介者将其加入内部列表(如users),同时同事持有中介者的引用; - 同事发送消息:同事调用自身的
send方法时,不直接与其他同事通信,而是将消息和自身引用传给中介者(mediator.sendMessage(message, this)); - 中介者转发消息:中介者根据预设规则(如 “发给所有其他同事”“按角色过滤”),遍历同事列表,调用目标同事的
receiveMessage方法,完成消息传递; - 同事接收消息:目标同事通过
receiveMessage处理消息,专注于自身业务,无需关心消息来自哪个同事或如何转发。
这种机制保证了:
- 同事间的完全解耦(仅通过中介者间接交互);
- 交互规则的集中管理(修改中介者即可调整所有交互);
- 系统的灵活性(新增同事只需注册,不影响现有逻辑)。
七、中介者模式的优缺点
优点:
- 降低耦合度:将多对多的网状关系简化为一对多的星状关系,消除同事间的直接依赖;
- 集中交互逻辑:所有交互规则在中介者中统一实现,便于维护和修改(如修改消息转发规则);
- 简化同事职责:同事只需关注自身业务,无需处理与其他对象的交互,符合单一职责原则;
- 提高扩展性:新增同事只需注册到中介者,无需修改其他同事或中介者的核心逻辑,符合开闭原则;
- 便于全局管理:中介者可统一实现日志记录、权限控制、事务管理等横切功能。
缺点:
- 中介者可能变得复杂:当同事数量多、交互规则复杂时,中介者会包含大量逻辑,成为 “大泥球”,难以维护;
- 中介者成为单点依赖:所有交互都通过中介者,若中介者出现故障,整个系统的交互会受影响;
- 同事与中介者耦合:同事需持有中介者的引用,若中介者接口变更,所有同事需同步修改;
- 可能降低性能:原本的直接调用变为 “同事→中介者→同事” 的间接调用,增加了一层转发开销。
八、适用场景
中介者模式适用于 “多个对象相互依赖、直接通信导致耦合过高” 的场景:
- 多对象交互频繁:如聊天室(用户间消息交互)、GUI 组件(按钮、输入框、列表的联动)、游戏中的角色互动(玩家、NPC、道具的相互影响);
- 对象间形成网状结构:当对象间的依赖关系复杂(多对多),且修改一个对象会影响多个其他对象时(如订单系统中的订单、库存、支付、物流的联动);
- 需要集中管理交互:如分布式系统中的协调器(协调多个节点的任务分配)、微服务中的服务网关(转发和协调服务间的调用);
- 希望简化对象职责:当对象因处理过多交互逻辑而变得臃肿时,可将交互逻辑抽离到中介者。
典型案例:
- 机场塔台系统(协调多架飞机的起降);
- 电商平台的订单处理中心(协调订单、库存、支付、物流系统);
- Swing 的
JOptionPane(作为对话框组件的中介者,协调按钮、文本框的交互); - 消息队列(如 RabbitMQ,作为生产者和消费者的中介者,转发消息)。
九、中介者模式 vs 观察者模式
两者都涉及对象间的间接通信,但核心目标和结构不同:
| 对比维度 | 中介者模式 | 观察者模式 |
|---|---|---|
| 核心目标 | 化解多对多的复杂交互,减少对象间直接依赖 | 实现一对多的联动,主题变化时自动通知观察者 |
| 通信方式 | 所有对象通过中介者双向通信(如 A→中介→B,B→中介→A) | 主题向观察者单向推送(主题→观察者,观察者不直接向主题推送) |
| 角色关系 | 中介者主动协调所有同事的交互(知道所有同事) | 主题被动通知观察者(不知道具体观察者,只依赖接口) |
| 适用场景 | 多对象相互依赖、交互复杂(聊天室、组件联动) | 一个对象变化需联动多个对象(公众号订阅、数据同步) |
十、总结
中介者模式的核心是 “引入中介,简化交互”,通过中介者对象协调多个同事的通信,将多对多的复杂依赖简化为一对多,解决了对象间直接耦合导致的维护困难和扩展受限问题。
它的关键是中介者对交互逻辑的集中管理(所有规则在中介者中实现)和同事与中介者的弱耦合(同事仅依赖中介者接口)。实际开发中,当系统中对象间的交互形成网状结构时,中介者模式是降低耦合、提升可维护性的最佳实践。
记住:中介者模式是对象间的 “交通枢纽”,让原本混乱的 “交叉路口” 变成有序的 “中转站”。
10 解释器模式(Interpreter Pattern)
一、什么是解释器模式?
解释器模式是行为型模式中专注于 “语言语法解析与执行”的核心模式,其核心思想是:定义一种语言的语法规则,并用解释器来解释该语言中的句子(表达式),将语法规则抽象为一系列解释器对象,通过组合这些对象来解析和执行复杂的语句。
简单说:“像计算器解析数学表达式一样,‘1+2*3’是一句‘语言’,计算器(解释器)先理解‘+’‘*’的语法规则(先乘后加),再将表达式拆分为‘1’‘2*3’‘+’等部分,逐步计算出结果”。
日常生活中,解释器模式的例子常见于 “规则解析” 场景:
- 正则表达式:引擎通过解释器解析
\d{3}-\d{4}等规则,匹配符合格式的字符串; - 计算器:解析 “3+5*(2-1)” 等算术表达式,按优先级计算结果;
- 配置文件解析:如解析
maxSize=1024; timeout=30等键值对配置,提取参数值; - 简单脚本语言:如游戏中的自定义脚本(“if 血量 < 30 then 释放技能”),通过解释器执行逻辑。
二、为什么需要解释器模式?(作用)
当系统中存在需要重复解析和执行特定格式的 “语言”(如表达式、规则) 时,直接硬编码解析逻辑会导致:
- 代码臃肿:解析逻辑与业务逻辑混杂,如判断运算符优先级、处理括号嵌套的代码会反复出现;
- 扩展困难:新增语法规则(如在算术表达式中添加 “^(幂运算)”)需修改大量现有解析代码,违反 “开闭原则”;
- 可读性差:复杂语法的解析逻辑(如嵌套表达式)会形成冗长的条件判断和循环,难以理解;
- 复用性低:相同的语法解析逻辑(如解析数字、加法)在不同场景中重复编写,无法复用。
解释器模式的核心作用是:
- 分离语法解析与执行:将语法规则抽象为解释器对象,解析逻辑与业务逻辑解耦;
- 模块化语法规则:每个语法单元(如数字、加法、括号)对应一个解释器,便于单独维护和扩展;
- 支持复杂语法组合:通过组合简单解释器构建复杂语法(如 “加法” 由 “数字” 和 “另一个表达式” 组合而成),符合 “组合复用原则”;
- 便于新增语法:新增规则只需新增解释器类,无需修改现有代码,符合 “开闭原则”。
三、反例:硬编码解析表达式的问题
假设我们要实现一个简单的计算器,支持 “+”“-” 运算和整数数字,例如解析 “1+2-3” 并计算结果。
不使用解释器模式的实现:
// 计算器类(硬编码解析逻辑,问题核心)
class SimpleCalculator {
// 解析并计算表达式(仅支持+、-和非负整数,无括号)
public int calculate(String expression) {
// 硬编码拆分表达式(假设运算符和数字用空格分隔,如"1 + 2 - 3")
String[] tokens = expression.split(" ");
if (tokens.length % 2 == 0) {
throw new IllegalArgumentException("表达式格式错误");
}
// 初始值为第一个数字
int result = Integer.parseInt(tokens[0]);
// 遍历运算符和数字,硬编码计算逻辑
for (int i = 1; i < tokens.length; i += 2) {
String operator = tokens[i];
int num = Integer.parseInt(tokens[i + 1]);
// 用if-else判断运算符,新增运算符需修改此处
if ("+".equals(operator)) {
result += num;
} else if ("-".equals(operator)) {
result -= num;
} else {
throw new UnsupportedOperationException("不支持的运算符:" + operator);
}
}
return result;
}
}
// 客户端:使用计算器
public class Client {
public static void main(String[] args) {
SimpleCalculator calculator = new SimpleCalculator();
try {
System.out.println("1 + 2 - 3 = " + calculator.calculate("1 + 2 - 3")); // 0
System.out.println("5 - 3 + 4 = " + calculator.calculate("5 - 3 + 4")); // 6
} catch (Exception e) {
e.printStackTrace();
}
// 问题:新增"*"运算符需修改calculate方法的if-else,违反开闭原则
// 问题:支持括号(如"1 + (2 - 3)")需大幅修改解析逻辑,代码会更臃肿
// 问题:解析逻辑与计算逻辑混杂,可读性差,难以维护
}
}
问题分析:
- 扩展困难:新增运算符(如 “*”“/”)时,必须修改
calculate方法的if-else分支,违反 “开闭原则”;若支持括号或运算符优先级(如先乘除后加减),需重写大量解析逻辑; - 可读性差:解析逻辑(拆分表达式、判断格式)与计算逻辑(处理运算符)混杂在一个方法中,当表达式复杂时(如嵌套括号),代码会变得冗长混乱;
- 复用性低:解析数字、处理运算符的逻辑无法复用,若其他场景(如表达式验证)需要类似解析,需重复编写;
- 耦合度高:表达式的格式(如必须用空格分隔)与解析逻辑硬绑定,若格式变更(如 “1+2-3” 无空格),需修改整个解析逻辑。
四、正例:用解释器模式解决问题
核心改进:将表达式的语法规则抽象为解释器对象(如数字解释器、加法解释器、减法解释器),通过组合这些解释器构建表达式的语法树,递归解析并计算结果,实现语法规则的模块化和可扩展。
解释器模式的实现(解析 “+”“-” 表达式):
import java.util.Stack;
// 1. 抽象表达式(Abstract Expression):定义解释方法
interface Expression {
int interpret(); // 解释并返回结果
}
// 2. 终结符表达式(Terminal Expression):解析数字(不可再拆分的最小语法单元)
class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() {
return number; // 数字的解释结果就是其本身
}
}
// 3. 非终结符表达式(Non-terminal Expression):解析加法(由两个表达式组合而成)
class AddExpression implements Expression {
private Expression left; // 左表达式(如"1")
private Expression right; // 右表达式(如"2")
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
// 解释结果 = 左表达式结果 + 右表达式结果
return left.interpret() + right.interpret();
}
}
// 4. 非终结符表达式:解析减法
class SubtractExpression implements Expression {
private Expression left;
private Expression right;
public SubtractExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() - right.interpret();
}
}
// 5. 解释器工具类:构建语法树(客户端使用,负责解析表达式字符串)
class ExpressionParser {
// 解析表达式(如"1 + 2 - 3")并返回根表达式
public static Expression parse(String expression) {
Stack<Expression> stack = new Stack<>();
String[] tokens = expression.split(" ");
for (String token : tokens) {
// 遇到运算符,弹出栈顶两个表达式组合
if ("+".equals(token)) {
Expression right = stack.pop();
Expression left = stack.pop();
stack.push(new AddExpression(left, right));
} else if ("-".equals(token)) {
Expression right = stack.pop();
Expression left = stack.pop();
stack.push(new SubtractExpression(left, right));
} else {
// 数字直接入栈
stack.push(new NumberExpression(Integer.parseInt(token)));
}
}
return stack.pop(); // 栈顶为根表达式
}
}
// 客户端:使用解释器解析表达式
public class Client {
public static void main(String[] args) {
// 解析"1 + 2 - 3"
Expression expr1 = ExpressionParser.parse("1 + 2 - 3");
System.out.println("1 + 2 - 3 = " + expr1.interpret()); // 0
// 解析"5 - 3 + 4"
Expression expr2 = ExpressionParser.parse("5 - 3 + 4");
System.out.println("5 - 3 + 4 = " + expr2.interpret()); // 6
// 新增"*"运算符:只需新增MultiplyExpression类,修改ExpressionParser支持"*"即可
// Expression expr3 = ExpressionParser.parse("2 * 3 + 4");
// System.out.println("2 * 3 + 4 = " + expr3.interpret()); // 10
}
}
改进效果:
- 语法规则模块化:数字、加法、减法分别由
NumberExpression、AddExpression、SubtractExpression实现,每个解释器专注于自身的解析逻辑,代码清晰,职责单一; - 支持灵活扩展:新增运算符(如 “”)时,只需新增
MultiplyExpression类实现Expression,并在ExpressionParser中添加对 “” 的处理,无需修改现有解释器,符合 “开闭原则”; - 可组合复杂语法:通过嵌套组合解释器,可解析复杂表达式(如 “(1 + 2) * (3 - 4)”),只需新增括号解释器和优先级处理逻辑,基础解释器可复用;
- 解析与执行分离:
ExpressionParser负责将字符串转换为语法树(解释器组合),interpret方法负责执行计算,两者职责分离,便于单独维护; - 逻辑复用:解释器对象可在多个场景中复用(如同一表达式多次计算、不同表达式共享数字解析逻辑)。
五、解释器模式的核心结构
解释器模式通过 “抽象语法树(AST)” 组合解释器,实现语法解析,包含 4 个核心角色:
- 抽象表达式(Abstract Expression):
- 定义所有解释器的统一接口,声明
interpret()方法(解释并返回结果); - 是所有具体表达式的父类,确保解释器以统一方式被调用。
- 定义所有解释器的统一接口,声明
- 终结符表达式(Terminal Expression):
- 对应语法中不可再拆分的最小单元(如算术表达式中的 “数字”、正则表达式中的 “单个字符”);
- 实现
interpret()方法,直接返回自身的解析结果(如NumberExpression返回数字值); - 没有子表达式,是语法树的叶子节点。
- 非终结符表达式(Non-terminal Expression):
- 对应语法中可拆分的复合规则(如 “加法”“减法”“括号表达式”);
- 持有一个或多个子表达式(
Expression对象),在interpret()中通过组合子表达式的结果得到自身结果(如AddExpression组合左右表达式的和); - 是语法树的非叶子节点,通过组合形成复杂语法。
- 环境(Context):
- 存储解释过程中需要共享的信息(如变量值、语法规则表),供所有解释器访问;
- 简化解释器之间的数据传递(如在表达式中解析变量 “x” 时,环境中存储 x 的值)。
- 客户端(Client):
- 负责构建抽象语法树:将输入的句子(如 “1+2-3”)解析为解释器的组合(如
SubtractExpression(AddExpression(1,2),3)); - 调用根表达式的
interpret()方法,触发整个语法树的解析和执行。
- 负责构建抽象语法树:将输入的句子(如 “1+2-3”)解析为解释器的组合(如
六、解释器模式的工作原理
解释器模式的核心是 “语法树的构建与递归解释”,流程如下:
- 定义语法规则:明确语言的语法(如 “表达式 = 数字 | 表达式 + 表达式 | 表达式 - 表达式”);
- 构建解释器:为每个语法规则创建对应解释器(终结符如
NumberExpression,非终结符如AddExpression); - 解析输入句子:客户端(或解析器工具类)将输入字符串(如 “1+2-3”)拆分为语法单元(“1”“+”“2”“-”“3”),按规则组合成抽象语法树(根节点为
SubtractExpression,左子节点为AddExpression,右子节点为NumberExpression(3)); - 递归解释执行:调用根表达式的
interpret()方法,根节点递归调用子节点的interpret(),最终汇总结果(如AddExpression先计算 1+2=3,再由SubtractExpression计算 3-3=0)。
这种机制保证了:
- 语法规则的模块化(每个规则对应一个解释器);
- 复杂语法的可组合性(通过嵌套子表达式构建);
- 解析逻辑的可扩展性(新增规则只需新增解释器)。
七、解释器模式的优缺点
优点:
- 语法规则模块化:每个语法单元对应独立解释器,便于理解、维护和复用;
- 易于扩展新语法:新增规则只需新增解释器类,无需修改现有代码,符合 “开闭原则”;
- 支持复杂语法组合:通过嵌套子表达式可构建任意复杂的语法(如多层括号表达式);
- 解析逻辑与业务分离:解释器专注于语法解析,业务逻辑(如计算、验证)通过
interpret方法实现,职责清晰。
缺点:
- 语法复杂时类膨胀:每新增一个语法规则就需新增一个解释器类,当语法复杂(如支持 10 种运算符 + 括号 + 变量)时,类数量会急剧增加,难以管理;
- 执行效率较低:递归解释会产生多次方法调用,且语法树的构建和遍历有额外开销,对于高性能要求的场景(如高频表达式计算)不适用;
- 只适用于简单语法:复杂语言(如 Java、SQL)的语法规则庞大且多变,用解释器模式实现会过于复杂,通常选择编译原理的词法 / 语法分析器(如 ANTLR)。
八、适用场景
解释器模式适用于 “需要解析和执行简单、重复出现的语法规则” 的场景:
- 简单表达式解析:如计算器的算术表达式(+、-、*、/、括号)、逻辑表达式(&&、||、!);
- 自定义规则验证:如验证密码格式(“长度≥8 且包含数字和字母”)、验证日期格式(“yyyy-MM-dd”);
- 领域特定语言(DSL):如简单的配置文件语法(
key=value;)、游戏脚本(“move player 10 steps”); - 重复出现的语法:当同一类语法在系统中多次出现(如报表工具中的公式计算),用解释器模式可复用解析逻辑。
典型案例:
- 正则表达式引擎(解析正则语法并匹配字符串);
- Excel 公式解析(如
SUM(A1:B2)*3的计算); - 简单规则引擎(如工作流中的条件判断 “if 金额> 1000 then 经理审批”);
- 配置文件解析器(如解析
log.level=INFO; timeout=30s)。
九、解释器模式 vs 组合模式
两者都通过 “组合对象” 构建树形结构,但核心目标不同:
| 对比维度 | 解释器模式 | 组合模式 |
|---|---|---|
| 核心目标 | 解析特定语言的语法规则,实现句子的解释与执行 | 统一单个对象与组合对象的操作,实现 “部分 - 整体” 结构的透明访问 |
| 树形结构意义 | 抽象语法树(AST),每个节点对应语法规则的解释器 | 组合树,每个节点是 “部分” 或 “整体”(如文件系统的文件和文件夹) |
| 方法核心 | interpret():解析并返回结果 |
operation():执行对象自身或组合的操作(如显示、删除) |
| 适用场景 | 语法解析(表达式、规则) | 部分 - 整体结构(文件系统、UI 组件树) |
十、总结
解释器模式的核心是 “语法规则的模块化解析”,通过将语言的语法抽象为一系列解释器对象,组合成抽象语法树来解析和执行句子,解决了简单语法场景下硬编码解析逻辑导致的扩展困难和复用性低的问题。
它的关键是抽象表达式接口的设计(统一interpret方法)和终结符 / 非终结符的合理拆分(最小语法单元与复合规则分离)。实际开发中,当需要处理重复出现的简单语法规则时,解释器模式是实现灵活解析的有效方案;但对于复杂语法,应优先选择专业的解析工具(如 ANTLR)。
记住:解释器模式是简单语法的 “翻译官”,让计算机能 “读懂” 并执行我们定义的 “语言”。
11 责任链模式(Chain of Responsibility Pattern)
一、什么是责任链模式?
责任链模式是行为型模式中专注于 “请求的链式传递与处理”的核心模式,其核心思想是:将多个请求处理者(对象)通过 “next” 引用连接成一条链,当请求发送时,沿着链依次传递,每个处理者根据自身职责决定 “处理该请求” 或 “将请求传递给下一个处理者”,最终实现请求的自动分发与处理。
简单说:“像公司的审批流程一样,员工请假单(请求)先给组长审批,组长无权处理(如超过 3 天)则传给经理,经理仍无权处理(如超过 7 天)则传给总监,直到有人处理或流程结束”。
日常生活中,责任链模式的例子随处可见:
- 客服工单系统:普通问题→一级客服→二级客服→专家客服,逐级升级处理;
- 日志系统:DEBUG 级别日志由控制台处理器处理,ERROR 级别由文件处理器处理,FATAL 级别由邮件处理器处理,按级别传递;
- 过滤器链:HTTP 请求依次经过认证过滤器、权限过滤器、日志过滤器,每个过滤器决定是否拦截或放行。
二、为什么需要责任链模式?(作用)
当系统中存在多个对象可能处理同一请求,但具体处理者不确定(需动态决定) 时,直接硬编码请求与处理者的对应关系会导致:
- 发送者与处理者强耦合:发送者需知道所有可能的处理者(如请假单需直接调用组长、经理、总监的审批方法),新增处理者需修改发送者代码;
- 逻辑分散且冗余:处理者的判断逻辑(如 “请假 3 天内组长批,3-7 天经理批”)分散在发送者中,若多个场景需要相同判断,会导致代码重复;
- 扩展困难:新增处理规则(如 “超过 15 天需 CEO 审批”)需修改发送者的条件判断,违反 “开闭原则”;
- 灵活性差:处理顺序固定(如必须先组长后经理),无法动态调整(如特殊情况直接传给总监)。
责任链模式的核心作用是:
- 解耦请求发送者与处理者:发送者只需将请求传给链的第一个节点,无需知道后续处理者是谁、如何处理;
- 动态分配处理责任:请求沿链传递,每个处理者自主决定是否处理,处理顺序可动态调整(如修改链的节点顺序);
- 支持灵活扩展:新增处理者只需添加到链中,无需修改现有处理者和发送者,符合 “开闭原则”;
- 集中处理逻辑:每个处理者专注于自身职责范围内的处理逻辑(如组长只处理 3 天内请假),职责单一。
三、反例:硬编码处理者的耦合问题
假设我们要实现一个请假审批系统,规则如下:
- 请假≤3 天:组长审批;
- 3 天 < 请假≤7 天:经理审批;
- 7 天 < 请假≤15 天:总监审批;
不使用责任链模式的实现:
// 1. 处理者:组长、经理、总监(各自有审批方法)
class GroupLeader {
public void approve(int days) {
if (days <= 3) {
System.out.println("组长批准请假" + days + "天");
} else {
System.out.println("组长无权批准,提交给经理");
// 直接调用经理的审批方法(强耦合)
new Manager().approve(days);
}
}
}
class Manager {
public void approve(int days) {
if (days <= 7) {
System.out.println("经理批准请假" + days + "天");
} else {
System.out.println("经理无权批准,提交给总监");
// 直接调用总监的审批方法(强耦合)
new Director().approve(days);
}
}
}
class Director {
public void approve(int days) {
if (days <= 15) {
System.out.println("总监批准请假" + days + "天");
} else {
System.out.println("请假超过15天,无人批准");
}
}
}
// 客户端:员工提交请假单(需知道从组长开始)
public class Client {
public static void main(String[] args) {
System.out.println("=== 请假2天 ===");
new GroupLeader().approve(2); // 组长批准
System.out.println("\n=== 请假5天 ===");
new GroupLeader().approve(5); // 组长→经理批准
System.out.println("\n=== 请假10天 ===");
new GroupLeader().approve(10); // 组长→经理→总监批准
// 输出:
// === 请假2天 ===
// 组长批准请假2天
//
// === 请假5天 ===
// 组长无权批准,提交给经理
// 经理批准请假5天
//
// === 请假10天 ===
// 组长无权批准,提交给经理
// 经理无权批准,提交给总监
// 总监批准请假10天
// 问题:新增CEO处理15天以上请假,需修改Director的approve方法,调用CEO
// 问题:处理顺序固定(必须组长→经理→总监),无法动态调整(如VIP员工直接找总监)
// 问题:处理者之间强耦合(GroupLeader直接依赖Manager),修改Manager类名会影响GroupLeader
}
}
问题分析:
- 强耦合:每个处理者直接引用下一个处理者(如
GroupLeader在代码中直接创建Manager),若处理者类名或方法名变更,所有依赖它的处理者都需修改; - 扩展困难:新增处理者(如 CEO)时,必须修改最后一个处理者(
Director)的代码,添加对 CEO 的调用,违反 “开闭原则”; - 处理顺序固定:链的顺序(组长→经理→总监)硬编码在处理者中,无法动态调整(如特殊情况跳过经理直接到总监);
- 责任分散:“谁该处理多少天” 的规则分散在各个处理者的
if判断中,若规则变更(如组长权限改为 5 天),需修改GroupLeader和Manager的判断条件,维护成本高。
四、正例:用责任链模式解决问题
核心改进:定义抽象处理者类,包含 “下一个处理者” 的引用和统一的处理方法;具体处理者实现自身处理逻辑,若无法处理则将请求传给下一个处理者;客户端构建处理链,将请求发给链的第一个节点,实现解耦和动态调整。
责任链模式的实现:
// 1. 抽象处理者(Handler):定义处理接口和下一个处理者的引用
abstract class Approver {
protected Approver nextApprover; // 下一个处理者
// 设置下一个处理者(构建链)
public void setNextApprover(Approver nextApprover) {
this.nextApprover = nextApprover;
}
// 抽象处理方法:子类实现具体逻辑
public abstract void approve(int days);
}
// 2. 具体处理者1:组长(处理≤3天)
class GroupLeader extends Approver {
@Override
public void approve(int days) {
if (days <= 3) {
System.out.println("组长批准请假" + days + "天");
} else {
System.out.println("组长无权批准,提交给下一级");
// 若有下一个处理者,则传递请求
if (nextApprover != null) {
nextApprover.approve(days);
} else {
System.out.println("无人处理该请求");
}
}
}
}
// 3. 具体处理者2:经理(处理3-7天)
class Manager extends Approver {
@Override
public void approve(int days) {
if (days > 3 && days <= 7) {
System.out.println("经理批准请假" + days + "天");
} else {
System.out.println("经理无权批准,提交给下一级");
if (nextApprover != null) {
nextApprover.approve(days);
} else {
System.out.println("无人处理该请求");
}
}
}
}
// 4. 具体处理者3:总监(处理7-15天)
class Director extends Approver {
@Override
public void approve(int days) {
if (days > 7 && days <= 15) {
System.out.println("总监批准请假" + days + "天");
} else {
System.out.println("总监无权批准,提交给下一级");
if (nextApprover != null) {
nextApprover.approve(days);
} else {
System.out.println("无人处理该请求");
}
}
}
}
// 客户端:构建责任链并发送请求
public class Client {
public static void main(String[] args) {
// 创建处理者
Approver groupLeader = new GroupLeader();
Approver manager = new Manager();
Approver director = new Director();
// 构建责任链:组长→经理→总监
groupLeader.setNextApprover(manager);
manager.setNextApprover(director);
// 发送请求(只需传给链的第一个节点)
System.out.println("=== 请假2天 ===");
groupLeader.approve(2); // 组长处理
System.out.println("\n=== 请假5天 ===");
groupLeader.approve(5); // 组长→经理处理
System.out.println("\n=== 请假10天 ===");
groupLeader.approve(10); // 组长→经理→总监处理
// 新增CEO处理15天以上(符合开闭原则)
System.out.println("\n=== 新增CEO处理15天以上 ===");
Approver ceo = new Approver() {
@Override
public void approve(int days) {
if (days > 15) {
System.out.println("CEO批准请假" + days + "天");
} else {
System.out.println("CEO只处理15天以上请假,提交给下一级");
if (nextApprover != null) nextApprover.approve(days);
}
}
};
// 调整链:总监→CEO
director.setNextApprover(ceo);
groupLeader.approve(20); // 组长→经理→总监→CEO处理
// 动态调整链:VIP员工直接找总监(无需修改处理者代码)
System.out.println("\n=== VIP员工直接找总监 ===");
director.approve(8); // 总监直接处理
}
}
改进效果:
- 解耦处理者与发送者:处理者之间通过
nextApprover松散连接,不再直接引用具体类(如GroupLeader不依赖Manager的类名),发送者只需将请求传给链的第一个节点,无需知道后续处理者; - 动态调整责任链:通过
setNextApprover可灵活修改处理顺序(如组长→总监→经理)或跳过某个处理者(如 VIP 直接找总监),无需修改处理者的内部逻辑; - 符合开闭原则:新增处理者(如 CEO)时,只需创建类实现
Approver,并添加到链中(director.setNextApprover(ceo)),现有处理者和客户端代码无需任何修改; - 职责单一:每个处理者只关注自身职责范围内的请求(组长处理≤3 天),判断逻辑集中在自身的
approve方法中,便于维护和修改(如调整组长权限只需改GroupLeader); - 请求自动传递:请求沿链自动传递,处理者无需硬编码下一个处理者的调用,只需判断
nextApprover是否存在并传递,代码更简洁。
五、责任链模式的核心结构
责任链模式通过 “链式连接 + 自主判断” 实现请求分发,包含 3 个核心角色:
- 抽象处理者(Handler):
- 定义所有处理者的统一接口,声明
handleRequest()(或approve())方法用于处理请求; - 持有下一个处理者(
nextHandler) 的引用,并提供setNext()方法用于构建链; - 是处理者的抽象父类,确保所有处理者以统一方式参与链的构建和请求传递。
- 定义所有处理者的统一接口,声明
- 具体处理者(Concrete Handler):
- 继承
Handler类,实现自身的处理逻辑(如 “组长处理≤3 天请假”); - 在
handleRequest()中判断是否能处理当前请求:- 若能处理:直接执行处理逻辑;
- 若不能处理:调用
nextHandler.handleRequest()将请求传递给下一个处理者(若存在);
- 专注于自身职责,不关心链的整体结构。
- 继承
- 客户端(Client):
- 负责创建具体处理者对象,并通过
setNext()构建责任链(如组长→经理→总监); - 将请求发送给链的第一个处理者,触发请求沿链传递;
- 无需知道请求的具体处理过程和最终处理者,只需关注链的入口。
- 负责创建具体处理者对象,并通过
六、责任链模式的工作原理
责任链模式的核心是 “请求沿链传递,自主决定处理”,流程如下:
- 构建责任链:客户端创建多个具体处理者,通过
setNext()方法将它们连接成链(如 A→B→C,A 的next是 B,B 的next是 C); - 发送请求:客户端将请求传递给链的第一个处理者(A);
- 处理或传递:
- 处理者 A 判断是否能处理请求:能处理则直接处理,流程结束;
- 若不能处理,A 调用
next(B)的处理方法,将请求传给 B; - 处理者 B 重复 A 的逻辑,直到某个处理者处理请求,或链结束(最后一个处理者无法处理,请求未被处理)。
这种机制保证了:
- 处理者之间的松散耦合(仅通过
next引用连接); - 请求处理的动态性(链的结构可随时调整);
- 处理逻辑的独立性(每个处理者只关心自己的职责)。
七、责任链模式的优缺点
优点:
- 解耦请求发送者与处理者:发送者无需知道处理者是谁、顺序如何,只需传给链的入口,降低耦合;
- 动态调整处理顺序:通过
setNext()可灵活修改链的结构(如新增、删除、调换处理者),适应不同场景; - 符合开闭原则:新增处理者只需添加到链中,无需修改现有代码,扩展灵活;
- 处理逻辑集中:每个处理者专注于自身职责,代码清晰,便于维护和修改;
- 支持多处理者协作:一个请求可被多个处理者处理(如日志同时被控制台和文件处理器记录),只需处理者处理后继续传递请求。
缺点:
- 请求可能未被处理:若链中所有处理者都无法处理请求,且未设置默认处理机制,请求会 “丢失”,需额外处理;
- 链过长影响性能:请求在长链中传递会产生多次方法调用,增加系统开销(如 10 个处理者的链,最多调用 10 次方法);
- 调试复杂:请求的处理路径随链的结构动态变化,出现问题时难以追踪具体处理过程;
- 可能产生循环依赖:若链中出现循环(如 A→B→A),会导致请求无限循环传递,引发系统异常。
八、适用场景
责任链模式适用于 “多个对象可能处理同一请求,且处理者不确定或需动态调整” 的场景:
- 分级审批流程:如请假审批(组长→经理→总监)、报销审批(部门→财务→总经理);
- 过滤器 / 拦截器链:如 Web 框架中的过滤器(认证→授权→日志→业务处理)、请求参数校验链(非空校验→格式校验→范围校验);
- 日志系统:按日志级别(DEBUG→INFO→WARN→ERROR)传递给不同处理器(控制台→文件→邮件);
- 异常处理:按异常类型(业务异常→系统异常→未知异常)传递给不同处理器(返回提示→记录日志→报警);
- 事件冒泡:如 GUI 组件的事件传递(按钮点击事件→面板→窗口,逐级向上传递)。
典型案例:
- Java 的
Servlet Filter(过滤器链,请求依次经过多个过滤器); - Spring 的
HandlerInterceptor(拦截器链,请求处理前 / 后经过多个拦截器); - 日志框架(如 Logback)的
Appender链(按级别传递日志到不同输出源); - 工作流引擎的节点流转(任务按流程节点依次传递给处理人)。
九、责任链模式 vs 装饰器模式
两者都通过 “对象链” 实现功能,但核心目标和行为不同:
| 对比维度 | 责任链模式 | 装饰器模式 |
|---|---|---|
| 核心目标 | 实现请求的分发处理,每个节点决定 “处理或传递” | 实现功能的动态增强,每个节点在不改变原对象的前提下添加新功能 |
| 链的作用 | 筛选处理者(只有一个或多个节点处理请求) | 叠加功能(所有节点都参与处理,增强原对象) |
| 节点关系 | 节点间是 “替代” 关系(处理者可替换下一个节点的处理) | 节点间是 “增强” 关系(装饰器包裹原对象,扩展功能) |
| 请求流向 | 请求沿链传递,直到被处理或结束(可能中途终止) | 请求必须经过所有装饰器(从外到内或从内到外),最终到达原对象 |
| 适用场景 | 审批、过滤、异常处理(分发请求) | 动态添加功能(如 IO 流的缓冲、加密装饰) |
十、总结
责任链模式的核心是 “链式传递,自主处理”,通过将多个处理者连接成链,让请求沿链传递并由合适的处理者处理,解决了请求发送者与处理者的强耦合问题,同时支持动态调整处理顺序和扩展新处理者。
它的关键是抽象处理者对 “下一个处理者” 的管理(setNext方法)和具体处理者对自身职责的判断(处理或传递)。实际开发中,当系统需要分级处理请求或动态调整处理流程时,责任链模式是提升灵活性和可维护性的最佳实践。
记住:责任链模式让请求像 “击鼓传花” 一样沿链传递,直到找到 “合适的人” 处理,过程中无需知道谁会最终接手。

浙公网安备 33010602011771号