1. 为什么你的代码越写越乱:状态判断与行为耦合的根源
1.1 一个天天见面的“状态驱动”场景
做业务开发的人对下面这类代码绝对不陌生:一个订单对象,根据状态字段决定当前能做什么事,if (state == 待支付) 执行支付逻辑,else if (state == 已发货) 执行确认收货逻辑,再往后还有退款、取消、售后,每一个分支里还嵌套着不同的业务操作。刚开始状态少,写得挺顺手,等状态从三个变成八个,每个状态下还有两三个动作时,函数体就开始膨胀。你不敢轻易动里面的逻辑,因为改一个分支可能牵扯到其他状态下的判断。这是典型的状态判断和业务行为被捆绑在一个方法里的问题。
状态模式(State Pattern)就是在这种场景下被提出来的一种行为型设计模式。它的核心思路是:把对象在不同状态下的行为,从一堆if-else/switch-case中拆出来,分散到各自独立的状态类里。对象内部状态变化时,行为也跟着变化,从外部看就像这个对象换了个类型一样。这个模式在状态机、协议解析、工作流引擎、游戏角色控制等场景里都有广泛应用。
1.2 状态模式到底在解决什么
很多人误以为状态模式只是为了少写几个if-else,这个理解太浅了。if-else本身不是问题,问题出在状态判断与业务逻辑的耦合上。
设想一个播放器对象:它在“停止”状态下点击“播放”按钮,应该开始播放;在“播放中”状态下重复点击“播放”按钮,可能没有任何响应或者弹提示;在“暂停”状态下点击“播放”,则恢复播放。同一个按钮事件,因为内部状态不同,执行结果完全不同。如果你不把“当前状态”当作一个一级公民来对待,而是散落在各个方法里到处判断,那么每次加一个状态,就要把所有相关方法都翻一遍,漏改一个就是线上事故。
状态模式解决的是“状态驱动的行为分发”问题。它把每一个状态封装成一个类,将“在这个状态下遇到这个事件应该做什么”和“做完之后转移到哪个状态”这两个信息内聚在一起。这带来的好处是:新增一个状态时,只需要新增一个状态类,并找到前一个状态里负责转移的地方做修改,而不是在一个大方法里到处加判断。
当然,状态模式不是银弹,它也有自己的适用边界。接下来我会从原理、代码、真实案例和坑位几个角度,把状态模式掰开揉碎讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 状态机的内部结构与状态模式的三要素
2.1 从状态机看状态模式
理解状态模式前,先理解状态机。状态机是一个数学模型,由“当前状态”“事件”“转移动作”“目标状态”四部分组成。以电梯为例:当前状态可能是“静止”,事件是“按下上行按钮”,转移动作是“关闭电梯门并向上启动”,目标状态是“上行中”。
状态机描述的是系统在不同状态下对外部事件的响应方式。如果用代码实现一个状态机,最直接的做法就是维护一个状态变量,再写一个大的事件分发方法,内部用分支判断当前状态,这就是最原始的“面条式”状态机。而状态模式,本质上是一种把状态机的“转移逻辑”和“行为逻辑”分散到各个状态对象里的面向对象实现手法。它不创造状态机,它只是让状态机更好维护、更好扩展。
2.2 Context、State与ConcreteState的职责划分
状态模式有三个角色,先理清它们各自干什么,后面看代码才不会迷路。
- Context(环境角色):持有当前状态对象,并对外提供业务方法。客户端直接面向Context编程,不用关心内部状态怎么流转。Context内部维护一个
State引用,所有请求都委托给当前状态对象处理。 - State(抽象状态角色):定义一个接口或抽象类,声明该状态下所有可能处理的业务行为。注意,这里的行为是“事件”,不是“动作”,比如
handle(Context context)或者更业务化的payOrder()、confirmReceipt()。 - ConcreteState(具体状态角色):实现State接口,负责真正处理对应状态下的事件,并决定事件处理后是否把Context切换到另一个状态。
一句话概括:Context负责“找谁干活”,State负责“干什么活、干完去哪”。两者通过依赖注入关联,切换状态时Context替换掉持有引用即可。
下面我用一个通用的例子展示三个角色之间的关系。先定义一个状态接口:
java复制public interface State {
void doAction(Context context);
}
Context持有一个状态对象,并提供切换状态的方法:
java复制public class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void request() {
state.doAction(this);
}
}
具体状态类实现业务逻辑并执行转移:
java复制public class ConcreteStateA implements State {
@Override
public void doAction(Context context) {
System.out.println("当前处于状态A,执行A特有的逻辑");
context.setState(new ConcreteStateB());
}
}
这样一轮操作下来,外部调用context.request()时,第一次执行的是状态A的逻辑,然后Context内部悄悄变成了状态B,第二次调用执行的就是状态B的逻辑。外部无感知,内部状态已经完成了一次转移。这其实就是状态模式最朴素的骨架。
3. 订单状态流转:if-else 实现和状态模式实现的直接对比
3.1 第一版:分支判断写法
先写一个大家都在用的订单类,状态用整数常量表示:
java复制public class Order {
public static final int STATUS_WAIT_PAY = 0;
public static final int STATUS_PAID = 1;
public static final int STATUS_SHIPPED = 2;
public static final int STATUS_FINISHED = 3;
public static final int STATUS_CANCELLED = 4;
private int status = STATUS_WAIT_PAY;
public void pay() {
if (status == STATUS_WAIT_PAY) {
System.out.println("支付成功,订单变为已支付状态");
status = STATUS_PAID;
} else if (status == STATUS_PAID) {
System.out.println("订单已支付,请勿重复支付");
} else if (status == STATUS_SHIPPED) {
System.out.println("订单已发货,不能支付");
} else {
System.out.println("当前状态不允许支付");
}
}
public void ship() {
if (status == STATUS_PAID) {
System.out.println("发货成功,订单变为已发货状态");
status = STATUS_SHIPPED;
} else {
System.out.println("当前状态不允许发货");
}
}
public void confirm() {
if (status == STATUS_SHIPPED) {
System.out.println("确认收货,订单完成");
status = STATUS_FINISHED;
} else {
System.out.println("当前状态不允许确认收货");
}
}
public void cancel() {
if (status == STATUS_WAIT_PAY) {
System.out.println("取消成功,订单变为已取消状态");
status = STATUS_CANCELLED;
} else {
System.out.println("当前状态不允许取消");
}
}
}
这段代码刚写完时没问题,但一旦需要新增一个“已退款”状态、或者加一个“卖家修改价格”功能,你就要在pay()、ship()、confirm()、cancel()里各加一个分支,而且还要小心状态之间的非法转移。比如STATUS_FINISHED状态下能不能退款,STATUS_CANCELLED能不能重新支付,这些规则散落在各方法里,很难一眼看全。我见过真实项目里一个订单类一千多行,全是这种状态分支的排列组合,维护的人痛不欲生。
3.2 第二版:状态模式写法
用状态模式重写,情况完全不同。先定义抽象状态类,把订单所有可能的行为都声明出来:
java复制public abstract class OrderState {
protected Order order;
public OrderState(Order order) {
this.order = order;
}
public abstract void pay();
public abstract void ship();
public abstract void confirm();
public abstract void cancel();
}
订单类本身作为Context,持有状态引用并提供默认行为兜底:
java复制public class Order {
private OrderState state;
public Order() {
state = new WaitPayState(this);
}
public void setState(OrderState state) {
this.state = state;
}
public void pay() {
state.pay();
}
public void ship() {
state.ship();
}
public void confirm() {
state.confirm();
}
public void cancel() {
state.cancel();
}
}
接下来是具体状态类。等待支付状态可以支付、可以取消,但不能发货和确认收货:
java复制public class WaitPayState extends OrderState {
public WaitPayState(Order order) {
super(order);
}
@Override
public void pay() {
System.out.println("支付成功");
order.setState(new PaidState(order));
}
@Override
public void ship() {
System.out.println("订单未支付,无法发货");
}
@Override
public void confirm() {
System.out.println("订单未支付,无法确认收货");
}
@Override
public void cancel() {
System.out.println("取消成功");
order.setState(new CancelledState(order));
}
}
已支付状态可以发货,也可以退款(如果业务上允许的话)调用方去走退款流程,但不能再支付:
java复制public class PaidState extends OrderState {
public PaidState(Order order) {
super(order);
}
@Override
public void pay() {
System.out.println("订单已支付,请勿重复支付");
}
@Override
public void ship() {
System.out.println("发货成功");
order.setState(new ShippedState(order));
}
@Override
public void confirm() {
System.out.println("订单未发货,无法确认收货");
}
@Override
public void cancel() {
System.out.println("已支付订单不能直接取消,请走退款流程");
}
}
已发货状态只能确认收货或异常处理,其他操作全部拒绝:
java复制public class ShippedState extends OrderState {
public ShippedState(Order order) {
super(order);
}
@Override
public void pay() {
System.out.println("订单已发货,不能支付");
}
@Override
public void ship() {
System.out.println("订单已发货,不能重复发货");
}
@Override
public void confirm() {
System.out.println("确认收货,订单完成");
order.setState(new FinishedState(order));
}
@Override
public void cancel() {
System.out.println("订单已发货,不能取消");
}
}
已完成和已取消状态相对简单,处理非法调用即可:
java复制public class FinishedState extends OrderState {
public FinishedState(Order order) {
super(order);
}
@Override
public void pay() {
System.out.println("订单已完成,不能支付");
}
@Override
public void ship() {
System.out.println("订单已完成,不能发货");
}
@Override
public void confirm() {
System.out.println("订单已完成,不能重复确认");
}
@Override
public void cancel() {
System.out.println("订单已完成,不能取消");
}
}
public class CancelledState extends OrderState {
public CancelledState(Order order) {
super(order);
}
@Override
public void pay() {
System.out.println("订单已取消,不能支付");
}
@Override
public void ship() {
System.out.println("订单已取消,不能发货");
}
@Override
public void confirm() {
System.out.println("订单已取消,不能确认收货");
}
@Override
public void cancel() {
System.out.println("订单已取消,不能重复取消");
}
}
3.3 两份代码的差异点逐条拆解
对比两份代码,有几个关键点值得反复体会。
行为内聚度不同。 if-else版本中,一个状态下所有行为的“规则”分散在多个方法里,看pay()方法时你需要知道当前其他状态是什么,才能判断这个分支是否成立。状态模式版本中,每个状态下能做什么、不能做什么、做完了去哪个状态,全部在同一个状态类里,查问题和加需求时只需要打开对应的类。
新增状态的代价不同。 给if-else版本新增一个“已退款”状态,你需要修改pay()、ship()、confirm()、cancel()四个方法,各加一个分支,还要考虑退出条件和非法路径。给状态模式版本新增“已退款”状态,只需要新建一个RefundedState类,然后找到能进入退款状态的原状态类,在其对应处理方法里加一行order.setState(new RefundedState(order))即可。其他状态类的代码完全不用碰。
对调用方的透明性相同。 调用方看到的都是order.pay()、order.cancel(),不需要感知订单内部是状态A还是状态B。这也是状态模式的价值所在:状态转移行为的复杂性被封装在内部。
当然,状态模式版本也有代价:类数量从1个变成6个,代码总行数明显增加。如果业务状态很少很稳定,比如只有草稿和发布两个状态,那用if-else完全合理,不要为了模式而模式。状态模式的收益在状态数量多、流转关系复杂、状态增减频繁的场景下才能放得最大。
4. 状态模式在Android中的一次真实落地:MediaPlayer状态管理
4.1 为什么Android官方喜欢用状态机管理多媒体组件
说到状态模式的实际应用,Android里的MediaPlayer是一个非常典型的教学案例。很多人在面试时被问“状态模式有哪些实际应用”,第一反应都是WifiManager或者Activity生命周期,但MediaPlayer的状态管理往往更能说明问题。
为什么MediaPlayer非要引入状态机?因为多媒体播放器的状态维度太多、非法操作太多。在没有准备好时调用start()会抛IllegalStateException,播放中调用prepare()会失败,释放后调用seekTo()直接崩溃。如果你在业务代码里自己维护这些判断,很难控制所有路径。Android官方干脆把MediaPlayer设计成一套严格的状态机:定义Idle、Initialized、Prepared、Started、Paused、Stopped、PlaybackCompleted、Error、Released等状态,状态之间只有合法的转移路径才能通过。
4.2 状态转移表与状态模式的对应关系
MediaPlayer的状态机比我上面订单的例子更严格,因为每个状态允许的事件组合非常多,而且大量状态会转移到Error状态。如果你去读Android框架层的MediaPlayer实现,会发现它不完全是经典的状态模式代码,而是类似状态机的状态枚举加大量判断,但设计思想是一脉相承的。
媒体播放器的一个片段:假设当前处于Prepared状态,调用start()后进入Started状态,此时如果音视频循环播放,播放完成后进入PlaybackCompleted状态;如果在Started状态下调用pause(),进入Paused状态,再调用start()又回到Started状态。这正好对应状态模式中“状态类处理事件并切换Context状态”的交互。
我在做音频播放器项目时,曾经参考这种设计把播放器状态封装为IdleState、PreparingState、PlayingState、PausedState、CompleteState。UI层调用player.play()时,内部由PreparingState或PausedState各自实现对应逻辑,外部不用关心当前状态是什么。最明显的好处是,“播放中再次点击播放”这类边界情况被每个状态类天然处理了,再也不会出现重复创建播放器导致的崩溃。
4.3 从《Android源码设计模式解析与实战》里的几个状态对象说起
很多Android开发者接触状态模式都是因为《Android源码设计模式解析与实战》这本书,里面用WifiStateMachine和GsmStateTracker等系统组件做了大量讲解。它们的核心思路是一致的:系统组件内部维护一个状态对象链表或栈,事件到达时由当前状态对象决定如何处理,处理完可能切换到另一个状态对象。
我自己在Android开发里更常遇到的状态模式变体是View的启用状态管理。比如自定义一个复合控件,它有“加载中”“数据为空”“网络错误”“展示数据”四种界面状态。如果用switch-case控制切到哪个页面、显示什么文案、绑什么点击事件,每新增一种状态都要改主控件代码。状态模式版本把每个界面状态独立成一个Controller对象,主控件只负责setState(new ErrorState(this))和获取当前状态,各页面逻辑完全隔离。这个改造在公司内部项目里真实落地过,后期加“空数据引导”和“权限申请”两个状态时,我只新增了两个类,主控件代码零改动。
需要提醒一句:Android里的状态机实现不一定都是严格的状态模式,很多是“状态模式思想结合工厂模式”。当状态类多到一定程度时,每个状态类里写一堆转移逻辑会变得臃肿,这时候就会考虑引入状态机框架或者表驱动的方式。这个后文再展开。
5. 状态模式和策略模式真的太像了:五个角度教你分辨
5.1 从类图看:几乎一样
状态模式(State Pattern)和策略模式(Strategy Pattern)在UML类图上的结构几乎一模一样:都是Context持有抽象接口,具体实现类各自实现接口,Context通过多态调用具体实现。你光看类图是无法区分两者的,这也是面试时最高频的对比问题,同时也是平时写代码时最容易模糊的地方。
5.2 从意图看:动机完全不同
策略模式解决的是“同一个需求有多个可替换的算法”问题,比如排序策略、支付方式、优惠力度计算。它讲究的是“可插拔”,用户主动选择一种算法,算法之间没有转移关系,选定了就是选定了,不会自动跳到另一个算法去。
状态模式解决的是“同一个对象在不同状态下行为不同”的问题。状态之间有流转关系,当前状态处理完事件后,可能会主动切换到另一个状态,外部对象无法也不应该控制状态如何流转,流转规则由状态对象内部决定。
一个简单的判断方式:如果Context的一个方法被调用后,自己内部的实现对象可能会被替换,那就是状态模式;如果实现对象始终由外部指定、不发生自动替换,那就是策略模式。
5.3 从调用关系看:谁触发转移
策略模式的Client主动从策略集合里挑选一个策略对象,传给Context。状态模式的Client通常只是在初始化时传入一个初始状态(或者Context自己创建),之后状态对象在方法执行过程中通过context.setState()自己完成切换。比如订单状态模式中,pay()方法内部把WaitPayState切换成PaidState,这个转移是状态类自己发起的,客户端根本不知道也不关心。
5.4 从封装粒度看:状态类不是一个算法
策略模式里每个策略类通常只负责一个完整的算法流程,它们彼此是平行关系。状态模式里每个状态类不仅包含当前状态下的行为代码,还包含“当前状态下哪些行为是合法的、哪些是非法的”这层元信息。状态模式的状态类之间不是平行的,它们是一张状态转移图上的节点,是有向边的连接关系。
5.5 一个反直觉的提示
写代码时,如果一个Context里有个字段叫state,另一个Context里也有个字段叫strategy,但代码结构完全一样,说明你还没真正理解两个模式的差异。状态模式应当关注“状态如何变化”,策略模式应当关注“行为如何选择”。如果你发现自己用策略模式写出的代码里,策略对象会自行修改Context的另一字段来影响后续行为,那大概率是你在用策略的外壳做状态的事,这种代码后期维护起来会很难受。
如果还不放心,可以记住这个实用判据:当你问自己“这个接口的实现是互斥可选、还是会动态变化”时,答案是前者用策略,后者用状态。
6. 状态模式实战中的六个坑与我的应对习惯
6.1 状态对象要不要共享
无状态的状态类可以做成单例。比如WaitPayState内部除了持有Context引用外没有自己的字段,那它其实不需要每次new一个对象,全局一个WaitPayState.INSTANCE就够用。但如果状态类内部需要维护该状态的额外属性,比如“已连接到网络的状态需要保存连接ID”,那就不能共享,得每个Context持有自己的状态对象实例。
我个人的习惯是:优先让状态类变成无状态的,保持线程安全。如果状态类需要记录临时数据,把它放到Context上,不要放到状态对象上。这样既能享受单例状态对象带来的内存节俭,又避免了并发共享状态互踩。
6.2 谁有权触发状态转移
状态模式实现中,状态对象调用context.setState()通常是标准操作,但没有强制规定。实践中要注意:不要让每个状态类都能无限制地把Context切到任意状态,否则时间一长状态流转关系会失控,和之前用if-else没有本质区别。
我的建议是:状态转移决策只放在状态类自身的方法里,并且严格按照业务规则来。如果发现某个状态类里出现了三四个setState(new XxxState(...))分支,就要考虑是不是真的需要这么多出口,或者这些分支能否抽象成一张转移表。必要时可以让某个“状态管理器”集中维护合法转移路径,状态类只负责业务动作,转移合法性由管理器校验。
6.3 状态类多了怎么办:表驱动与状态机框架
状态数量超过十个时,手写状态类的维护成本会急剧上升。每个状态类都是一个小文件,找转移关系要一个个翻,脑子的负担比if-else还大。这时候我建议:
- 表驱动:把状态转移关系写进一张二维表或Map里,用事件作为key查询目标状态;
- 状态机框架:如Spring StateMachine、Copper等,把状态定义、事件、转移条件、动作统一管理,覆盖复杂流程编排。
但这不等于状态模式被否定了。状态模式适合状态数量适中(5到10个)、行为差异明显的对象;状态数量爆炸、转移规则复杂时,完全可以用状态机框架从更上层接管。作为学习者和刚入门的设计模式爱好者,先把状态模式的人肉实现练熟,再谈框架,路径会更顺。
6.4 日常开发中最适合用状态模式的三类场景
第一类是生命周期明确的组件,比如播放器、网络连接管理器、下载任务、页面导航控制器;第二类是业务流程清晰、状态流转频繁的业务对象,比如订单、审批流、工单;第三类是协议解析或者游戏角色状态,输入事件多样、输出行为差异巨大,状态模式天然契合。
如果你遇到的是一个根本没有状态概念的场景,强行套状态模式只会增加类数量。要记住,设计模式是为代码的可读性、可维护性服务的,不是为了凑一个高大上的架构名词。
我见过太多人看完状态模式的示例代码,激动地说“我终于可以不用写if-else了”,然后在订单类里花了三个小时把状态改成了一堆类,结果发现老板临时让加一个“管理员强制关闭订单”的按钮,需要让已经发货的订单也能直接关闭,这时候你可能会发现状态类过多、转移路径调整啰嗦,远不如直接改if-else来得快。所以用模式之前,先算清楚当前状态数量、变更频率、团队维护成本这三笔账。状态少变更频率低,if-else更划算;状态多且流转频繁才值得上状态模式。
我自己在多次项目里养成的一个小习惯:一开始用if-else把业务流程跑通,等状态确实增加到第四个以上,或者发现同一种状态判断在代码里出现了三个以上的重复位置,才动手重构为状态模式。这个时间点往往是最适合的,既不会过早过度设计,也不会等代码烂到没法收拾才醒悟。
