大家好,欢迎来到设计模式系列文章(基础篇)的第三十一篇内容。在上一篇中,我们学习了行为型模式的第二十种常用模式------观察者模式,其核心是定义对象间的一对多依赖关系,实现被观察者状态变化时的联动通知,解耦依赖关系,广泛应用于消息通知、事件驱动等场景。今天,我们将学习行为型模式的第二十一种常用模式------状态模式,它的核心是允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式专注于"对象状态与行为的联动",将状态的判断和行为的实现分离,让代码更清晰、更具扩展性,轻松应对状态频繁变化的业务场景。
在日常开发和生活中,状态模式的应用无处不在。比如我们常用的播放器,它有"播放""暂停""停止"三种状态:处于"播放"状态时,点击按钮会执行"暂停"操作,切换到"暂停"状态;处于"暂停"状态时,点击按钮会执行"继续播放"或"停止"操作,切换到对应状态;处于"停止"状态时,点击按钮会执行"开始播放"操作,切换到"播放"状态。播放器的行为完全由当前状态决定,状态变化时,行为也会随之改变。
再比如电商系统中的订单,从创建到完成会经历"待付款""已付款""待发货""已发货""已完成""已取消"等多种状态,不同状态下的订单能执行的操作完全不同:"待付款"状态可执行"付款""取消订单"操作;"已付款"状态可执行"申请退款""催单"操作;"已发货"状态可执行"确认收货"操作。如果我们用传统的if-else或switch-case判断订单状态,再执行对应操作,会导致代码臃肿、耦合度高,新增状态时需要修改大量代码,违背开闭原则。
状态模式就完美解决了这一痛点:它将每个状态对应的行为封装成独立的状态类,对象的状态由状态类管理,当对象状态变化时,会切换到对应的状态类,从而执行不同的行为。这样一来,无需大量的条件判断,新增状态时只需新增对应的状态类,无需修改原有代码,大幅提升了代码的可维护性和扩展性。今天,我们就从核心定义、结构、实战实现、场景对比、避坑指南全维度讲解,帮大家彻底掌握这种"状态驱动行为"的实用设计模式。
一、状态模式的核心定义与设计初衷
1. 核心定义
状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。状态模式的核心是将对象的状态与对应的行为分离,封装成独立的状态类,由状态类管理对象的行为,实现状态与行为的联动,简化状态判断逻辑。
通俗理解:状态模式就像我们生活中的"角色切换",一个人在不同的场景(状态)下,会表现出不同的行为:在公司是"员工"状态,行为是上班、工作、开会;在家是"家人"状态,行为是做饭、休息、陪伴家人;在商场是"消费者"状态,行为是逛街、购物、付款。这个人(上下文对象)的行为,由当前的状态决定,状态切换时,行为也会随之改变,而这个人本身并没有"修改",只是切换了状态。再比如电梯,有"停止""上升""下降"三种状态,不同状态下的电梯,能执行的操作不同(停止时可开门、关门;上升时只能继续上升或停止;下降时只能继续下降或停止),电梯的行为由当前状态驱动。
2. 设计初衷(解决的核心问题)
状态模式的出现,核心是解决"对象状态多、行为随状态变化,且用条件判断实现状态与行为联动,导致代码臃肿、耦合度高"的痛点,具体解决3个核心问题:
- 简化状态判断:将状态对应的行为封装成独立的状态类,无需使用大量的if-else或switch-case判断状态,降低代码复杂度;
- 解耦状态与行为:状态的变化和对应的行为实现分离,状态类负责管理自身对应的行为,上下文对象只需切换状态,无需关心行为的具体实现;
- 提升扩展性:新增状态时,只需新增对应的状态类,实现该状态下的行为,无需修改原有代码,符合开闭原则;状态的切换逻辑可灵活调整,不影响上下文对象。
3. 设计原则适配
状态模式严格贴合面向对象设计核心原则,尤其在"开闭原则"和"单一职责原则"上表现突出,是实现状态与行为联动的最佳实践:
- 开闭原则:新增状态时,只需新增对应的状态类,无需修改上下文对象和原有状态类,扩展性强;
- 单一职责原则:每个状态类只负责自身状态对应的行为,上下文对象只负责维护当前状态和状态切换,各司其职,避免职责耦合;
- 依赖倒转原则:上下文对象依赖抽象状态类,不依赖具体状态类;具体状态类实现抽象状态接口,便于替换和扩展;
- 迪米特法则(最少知道原则):上下文对象只需知道当前状态类,无需知道其他状态类的实现;状态类之间互不依赖,降低耦合度。
二、状态模式的核心结构(3个核心角色)
状态模式的结构围绕"上下文对象维护状态、状态类实现行为、状态切换驱动行为变化"展开,核心包含3个角色,各司其职、协同完成状态与行为的联动,我们以"播放器状态管理"为场景,逐一拆解角色职责与交互逻辑:
1. 上下文(Context)
维护一个当前状态的引用,是状态模式的核心调度者,负责与客户端交互,接收客户端的请求,并将请求委托给当前状态类处理;同时提供状态切换的方法,实现状态的切换。上下文对象不直接实现状态对应的行为,而是由当前状态类实现,自身只负责状态管理和请求委托。对应播放器场景中的"播放器对象",维护当前的播放状态(播放、暂停、停止),接收用户的操作请求(点击播放、暂停、停止),并委托给当前状态类处理。
2. 抽象状态(State)
所有具体状态类的抽象父类或接口,定义了状态对应的行为规范,声明了所有状态都需要实现的核心方法(如播放器的播放、暂停、停止方法)。抽象状态规范了不同状态的行为接口,确保所有具体状态类都能提供统一的行为接口,便于上下文对象委托请求。对应播放器场景中的"抽象播放状态",定义播放、暂停、停止的行为接口。
3. 具体状态(Concrete State)
实现抽象状态接口,封装了该状态下对象的具体行为,同时负责判断状态切换的条件,在合适的时机通知上下文对象切换到其他状态。具体状态类是状态模式的核心,每个具体状态对应一套独立的行为逻辑,且负责状态切换的判断。对应播放器场景中的"播放状态""暂停状态""停止状态",分别实现各自状态下的播放、暂停、停止行为,以及状态切换逻辑。
核心关系总结:抽象状态定义行为规范,具体状态实现行为逻辑和状态切换判断;上下文对象维护当前状态的引用,接收客户端请求并委托给当前状态类处理;当具体状态类判断需要切换状态时,通知上下文对象切换到新的状态,上下文对象更新当前状态后,后续的请求将委托给新的状态类处理,实现状态与行为的联动。
三、状态模式的核心逻辑与执行流程
状态模式的核心逻辑是"状态驱动行为,状态切换联动行为变化",标准执行流程分为六步,全程实现状态与行为的解耦,逻辑闭环,我们以"播放器状态切换"为例,拆解执行流程:
- 定义抽象状态接口:声明该状态下对象需要实现的所有行为方法;
- 实现具体状态类:每个具体状态类实现抽象状态接口,封装自身状态对应的行为,同时判断状态切换条件;
- 定义上下文类:维护当前状态的引用,提供状态切换方法,接收客户端请求并委托给当前状态类处理;
- 初始化上下文对象:为上下文对象设置初始状态(如播放器初始状态为"停止状态");
- 客户端发起请求:客户端向上下文对象发送请求(如点击播放按钮);
- 状态处理与切换:上下文对象将请求委托给当前状态类处理,具体状态类执行对应行为,若满足状态切换条件,通知上下文对象切换到新状态,上下文对象更新当前状态,完成状态与行为的联动。
关键要点:上下文对象不直接处理请求,而是委托给当前状态类处理,实现了状态与行为的解耦;每个具体状态类只负责自身状态的行为和切换逻辑,无需关心其他状态;新增状态时,只需新增具体状态类,无需修改上下文对象和原有状态类,符合开闭原则;状态切换由具体状态类判断,上下文对象只需执行切换操作,简化了状态管理逻辑。
四、状态模式的实战实现(播放器状态管理场景)
我们以高频的播放器状态管理为场景,使用Java代码实现状态模式,播放器(上下文)支持播放、暂停、停止三种操作,对应"播放状态""暂停状态""停止状态"三种状态;不同状态下,执行相同操作会有不同的行为(如暂停状态下点击暂停,会提示"已处于暂停状态"),且能自动切换到对应状态,直观体现状态模式"状态驱动行为、灵活管理状态"的核心优势。
场景说明:播放器初始状态为"停止状态";停止状态下,点击播放按钮,切换到播放状态并执行播放行为,点击暂停/停止按钮提示"当前处于停止状态,无法执行该操作";播放状态下,点击暂停按钮,切换到暂停状态并执行暂停行为,点击停止按钮,切换到停止状态并执行停止行为,点击播放按钮提示"已处于播放状态";暂停状态下,点击播放按钮,切换到播放状态并执行继续播放行为,点击停止按钮,切换到停止状态并执行停止行为,点击暂停按钮提示"已处于暂停状态"。
1. 第一步:定义抽象状态接口(State)
bash
// 抽象状态:抽象播放状态,定义播放器的核心行为接口
public interface PlayerState {
// 播放操作
void play(PlayerContext player);
// 暂停操作
void pause(PlayerContext player);
// 停止操作
void stop(PlayerContext player);
}
2. 第二步:实现具体状态类(Concrete State)
(1)播放状态
bash
// 具体状态:播放状态,实现播放状态下的行为和状态切换逻辑
public class PlayingState implements PlayerState {
// 播放状态下执行播放操作:提示已在播放,不切换状态
@Override
public void play(PlayerContext player) {
System.out.println("【播放状态】已处于播放中,无需重复播放");
}
// 播放状态下执行暂停操作:切换到暂停状态,执行暂停行为
@Override
public void pause(PlayerContext player) {
System.out.println("【播放状态】执行暂停操作...");
// 切换状态为暂停状态
player.setState(new PausedState());
System.out.println("状态切换成功,当前状态:暂停状态");
}
// 播放状态下执行停止操作:切换到停止状态,执行停止行为
@Override
public void stop(PlayerContext player) {
System.out.println("【播放状态】执行停止操作...");
// 切换状态为停止状态
player.setState(new StoppedState());
System.out.println("状态切换成功,当前状态:停止状态");
}
}
(2)暂停状态
bash
// 具体状态:暂停状态,实现暂停状态下的行为和状态切换逻辑
public class PausedState implements PlayerState {
// 暂停状态下执行播放操作:切换到播放状态,执行继续播放行为
@Override
public void play(PlayerContext player) {
System.out.println("【暂停状态】执行继续播放操作...");
// 切换状态为播放状态
player.setState(new PlayingState());
System.out.println("状态切换成功,当前状态:播放状态");
}
// 暂停状态下执行暂停操作:提示已暂停,不切换状态
@Override
public void pause(PlayerContext player) {
System.out.println("【暂停状态】已处于暂停中,无需重复暂停");
}
// 暂停状态下执行停止操作:切换到停止状态,执行停止行为
@Override
public void stop(PlayerContext player) {
System.out.println("【暂停状态】执行停止操作...");
// 切换状态为停止状态
player.setState(new StoppedState());
System.out.println("状态切换成功,当前状态:停止状态");
}
}
(3)停止状态
bash
// 具体状态:停止状态,实现停止状态下的行为和状态切换逻辑
public class StoppedState implements PlayerState {
// 停止状态下执行播放操作:切换到播放状态,执行播放行为
@Override
public void play(PlayerContext player) {
System.out.println("【停止状态】执行播放操作...");
// 切换状态为播放状态
player.setState(new PlayingState());
System.out.println("状态切换成功,当前状态:播放状态");
}
// 停止状态下执行暂停操作:提示无法执行,不切换状态
@Override
public void pause(PlayerContext player) {
System.out.println("【停止状态】当前处于停止状态,无法执行暂停操作");
}
// 停止状态下执行停止操作:提示已停止,不切换状态
@Override
public void stop(PlayerContext player) {
System.out.println("【停止状态】已处于停止中,无需重复停止");
}
}
3. 第三步:实现上下文类(Context)
bash
// 上下文:播放器对象,维护当前状态,委托请求给当前状态类处理
public class PlayerContext {
// 维护当前状态的引用(默认初始状态为停止状态)
private PlayerState currentState;
// 构造方法:初始化初始状态
public PlayerContext() {
this.currentState = new StoppedState();
System.out.println("播放器初始化,当前状态:停止状态");
}
// 切换状态的方法
public void setState(PlayerState state) {
this.currentState = state;
}
// 播放操作:委托给当前状态类处理
public void play() {
currentState.play(this);
}
// 暂停操作:委托给当前状态类处理
public void pause() {
currentState.pause(this);
}
// 停止操作:委托给当前状态类处理
public void stop() {
currentState.stop(this);
}
// 辅助方法:获取当前状态(用于测试)
public String getCurrentState() {
if (currentState instanceof PlayingState) {
return "播放状态";
} else if (currentState instanceof PausedState) {
return "暂停状态";
} else if (currentState instanceof StoppedState) {
return "停止状态";
}
return "未知状态";
}
}
4. 第四步:客户端测试
bash
// 客户端测试:播放器状态管理场景
public class StatePatternTest {
public static void main(String[] args) {
// 1. 创建上下文对象(播放器)
PlayerContext player = new PlayerContext();
System.out.println("\n=== 测试1:停止状态下执行操作 ===");
player.play(); // 停止状态 -> 播放状态
player.pause(); // 播放状态 -> 暂停状态
player.stop(); // 暂停状态 -> 停止状态
System.out.println("\n=== 测试2:播放状态下执行操作 ===");
player.play(); // 停止状态 -> 播放状态
player.play(); // 播放状态,重复播放
player.stop(); // 播放状态 -> 停止状态
System.out.println("\n=== 测试3:暂停状态下执行操作 ===");
player.play(); // 停止状态 -> 播放状态
player.pause(); // 播放状态 -> 暂停状态
player.play(); // 暂停状态 -> 播放状态
player.pause(); // 播放状态 -> 暂停状态
player.pause(); // 暂停状态,重复暂停
player.stop(); // 暂停状态 -> 停止状态
System.out.println("\n=== 测试4:停止状态下执行无效操作 ===");
player.pause(); // 停止状态,无法暂停
player.stop(); // 停止状态,重复停止
// 打印最终状态
System.out.println("\n播放器最终状态:" + player.getCurrentState());
}
}
运行结果
bash
播放器初始化,当前状态:停止状态
=== 测试1:停止状态下执行操作 ===
【停止状态】执行播放操作...
状态切换成功,当前状态:播放状态
【播放状态】执行暂停操作...
状态切换成功,当前状态:暂停状态
【暂停状态】执行停止操作...
状态切换成功,当前状态:停止状态
=== 测试2:播放状态下执行操作 ===
【停止状态】执行播放操作...
状态切换成功,当前状态:播放状态
【播放状态】已处于播放中,无需重复播放
【播放状态】执行停止操作...
状态切换成功,当前状态:停止状态
=== 测试3:暂停状态下执行操作 ===
【停止状态】执行播放操作...
状态切换成功,当前状态:播放状态
【播放状态】执行暂停操作...
状态切换成功,当前状态:暂停状态
【暂停状态】执行继续播放操作...
状态切换成功,当前状态:播放状态
【播放状态】执行暂停操作...
状态切换成功,当前状态:暂停状态
【暂停状态】已处于暂停中,无需重复暂停
【暂停状态】执行停止操作...
状态切换成功,当前状态:停止状态
=== 测试4:停止状态下执行无效操作 ===
【停止状态】当前处于停止状态,无法执行暂停操作
【停止状态】已处于停止中,无需重复停止
播放器最终状态:停止状态
从运行结果可以看出,状态模式成功实现了播放器的状态管理:播放器(上下文)维护当前状态,接收用户操作请求后,委托给当前状态类处理;不同状态下执行相同操作,会有不同的行为反馈,且能自动切换到对应状态。例如,停止状态下点击播放,会切换到播放状态并执行播放行为;播放状态下点击暂停,会切换到暂停状态并执行暂停行为,完全符合预期。同时,状态的切换逻辑由具体状态类实现,上下文对象无需关心状态判断和行为实现,代码清晰、简洁;新增状态(如"快进状态")时,只需新增对应的具体状态类,无需修改播放器和原有状态类,符合开闭原则,扩展性极强。
五、状态模式的高频应用场景
状态模式适用于所有对象状态多、行为随状态变化,且需要灵活管理状态切换的业务场景,核心作用是简化状态判断、解耦状态与行为,以下是四大高频落地场景:
1. 状态流转频繁的业务对象(最经典应用)
各类业务对象存在多种状态,且状态流转频繁,行为随状态变化,是状态模式最核心的应用场景:
- 订单状态管理:电商订单的"待付款""已付款""待发货""已发货""已完成""已取消"等状态,不同状态下的操作的行为不同,状态流转频繁;
- 播放器状态管理:如实战场景中的播放器,"播放""暂停""停止""快进""快退"等状态,行为随状态变化;
- 工单状态管理:企业工单的"待处理""处理中""已完成""已驳回"等状态,不同状态下的处理行为不同。
2. 设备状态控制
各类设备存在多种运行状态,不同状态下的设备行为不同,需要灵活管理状态切换:
- 电梯状态控制:电梯的"停止""上升""下降""开门""关门"等状态,不同状态下的操作(如按楼层按钮、开门按钮)行为不同;
- 空调状态控制:空调的"开机""关机""制冷""制热""送风"等状态,不同状态下的运行行为不同;
- 打印机状态控制:打印机的"空闲""打印中""缺纸""故障"等状态,不同状态下的操作行为不同。
3. 流程引擎与状态机
流程引擎和状态机中,核心是状态的流转和行为的联动,状态模式是核心设计思想: - 工作流引擎:企业工作流(如请假流程、审批流程)的"提交""审核中""审核通过""审核驳回"等状态,状态流转驱动流程推进;
- 状态机框架:如Spring StateMachine,底层基于状态模式,实现复杂的状态流转和行为管理;
- 游戏角色状态管理:游戏角色的"正常""受伤""死亡""眩晕"等状态,不同状态下的角色行为(如移动、攻击)不同。
4. 框架底层与工具类设计
主流开源框架和工具类中,大量使用状态模式实现状态管理和行为联动,提升框架的灵活性:
- Spring StateMachine:Spring提供的状态机框架,基于状态模式,实现复杂的状态流转和事件驱动;
- Java线程状态管理:Java线程的"新建""就绪""运行""阻塞""终止"等状态,线程的行为随状态变化,底层隐含状态模式思想;
- 前端组件状态管理:Vue、React等前端框架的组件状态(如loading、success、error),组件的渲染行为随状态变化,本质是状态模式的应用。
六、状态模式 vs 观察者模式(重点区分)
状态模式与上一篇学习的观察者模式,都属于行为型模式,且都涉及"对象状态变化与行为联动",但核心目标、使用场景、核心逻辑完全不同,极易混淆,通过表格清晰对比,帮大家彻底区分:
| 对比维度 | 状态模式 | 观察者模式 |
|---|---|---|
| 核心目标 | 管理对象自身的状态,让行为随状态变化,简化状态判断逻辑 | 定义一对多依赖,实现被观察者状态变化时的联动通知,解耦依赖 |
| 核心逻辑 | 上下文维护当前状态,请求委托给当前状态类处理,状态切换驱动行为变化 | 被观察者维护观察者列表,状态变化时通知所有观察者,观察者执行更新 |
| 适用场景 | 对象状态多、行为随状态变化,需要灵活管理状态切换(关注"自身状态管理") | 存在一对多依赖、需要联动通知,解耦被观察者与观察者(关注"跨对象联动") |
| 核心角色 | 上下文、抽象状态、具体状态 | 抽象被观察者、具体被观察者、抽象观察者、具体观察者 |
| 核心关注点 | 对象自身的状态与行为的联动,简化状态判断 | 多个对象间的依赖联动,实现状态变化的通知 |
一句话总结:状态模式管"对象自身状态与行为的联动",观察者模式管"多个对象间的状态联动通知";对象自身状态多、行为随状态变化用状态模式,多个对象间存在依赖、需要联动通知用观察者模式,两者可结合使用(如订单状态变化时,通过状态模式管理订单自身状态与行为,同时通过观察者模式通知库存、物流等其他模块)。
七、状态模式的常见坑与避坑指南
坑1:状态过多,导致状态类冗余
若对象的状态数量过多(如几十种状态),使用状态模式会导致新增大量的具体状态类,造成代码冗余、难以维护,违背状态模式的设计初衷。
避坑指南:对状态进行分类合并,将相似状态合并为一个状态类,减少状态类数量;若状态确实过多,可结合策略模式,将状态对应的行为抽离为策略类,状态类只负责状态切换,行为由策略类实现;或使用状态机框架(如Spring StateMachine),简化状态管理。
坑2:状态切换逻辑混乱,导致状态异常
部分开发者在实现时,将状态切换逻辑分散在上下文对象和多个状态类中,导致状态切换逻辑混乱,出现状态切换异常(如从停止状态直接切换到播放状态以外的状态)。
避坑指南:将状态切换逻辑统一放在具体状态类中,上下文对象只负责执行状态切换,不参与状态切换的判断;每个具体状态类只负责自身能切换到的状态,明确状态流转规则(如停止状态只能切换到播放状态);可通过状态流转图,明确所有状态的切换路径,避免混乱。
坑3:上下文与状态类耦合过高
部分开发者在实现时,让上下文对象直接依赖具体状态类(如在上下文对象中直接new具体状态类),或状态类直接操作上下文对象的私有属性,导致上下文与状态类耦合过高,难以扩展。
避坑指南:上下文对象依赖抽象状态类,不依赖具体状态类;状态类通过上下文对象提供的公共方法操作上下文,不直接访问上下文的私有属性;状态的创建可通过工厂模式,由工厂类创建具体状态类,降低上下文与状态类的耦合。
坑4:过度使用状态模式,简化场景复杂化
部分开发者在对象状态少、行为简单的场景中(如只有2种状态,行为逻辑简单),强行使用状态模式,新增上下文、抽象状态、具体状态等角色,导致代码冗余、逻辑晦涩,反而降低了开发效率。
避坑指南:状态数量少(2-3种)、行为逻辑简单的场景,直接使用if-else或switch-case判断即可,无需过度设计;只有当状态数量多、状态流转频繁、行为逻辑复杂时,才使用状态模式。
坑5:忽略状态的持久化需求
部分场景中,需要将对象的当前状态持久化(如订单状态、播放器进度),若未实现状态的持久化,系统重启后状态会丢失,导致业务逻辑异常。
避坑指南:在上下文对象中添加状态持久化方法(如将当前状态存储到数据库、文件);状态类实现序列化接口,支持状态的序列化和反序列化;系统重启时,从持久化存储中读取状态,恢复上下文对象的当前状态。
八、系列文章预告
本篇文章,我们详细讲解了状态模式的核心定义、三个核心角色、标准执行流程、播放器状态管理实战代码、高频应用场景和避坑指南,同时区分了易混淆的观察者模式。状态模式凭借"状态驱动行为、简化状态判断、灵活扩展"的优势,成为状态流转频繁、行为随状态变化场景的首选设计模式,能够大幅提升代码的可维护性和扩展性,也是流程引擎、设备控制、状态机框架的核心设计思想之一。
下一篇,我们将学习行为型模式的第二十二种常用模式------策略模式,它的核心是定义一系列算法,将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式专注于"算法的封装与切换",广泛应用于多种算法可选、需要灵活切换算法的场景,如排序算法切换、支付方式切换、日志输出方式切换等。
策略模式------算法封装切换,提升代码灵活性。我们不见不散!