22|状态:让合法动作随生命周期变化
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.state。
系列导航:Java 25 设计模式学习指南
文章处于草稿、已发布或已归档状态。草稿允许发布,已发布允许归档,已归档不能再次发布。若 publish 和 archive 各自包含一大段状态判断,每新增一个状态就要检查许多方法是否同步修改。
状态模式把“在某个状态下如何响应动作”放进该状态对象。上下文保留对外操作入口,并把当前动作交给当前状态处理。
从问题提炼设计意图
状态模式允许对象在内部状态变化时改变行为,把状态相关行为和合法迁移封装到独立状态类中。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
Manuscript |
上下文,持有当前状态并暴露业务动作 |
PublicationState |
状态接口,声明发布、归档和名称 |
DraftState、PublishedState、ArchivedState |
具体状态,决定每个动作与迁移 |
Demo |
调用业务动作,不直接设置状态 |
classDiagram
Manuscript --> PublicationState : current
PublicationState <|.. DraftState
PublicationState <|.. PublishedState
PublicationState <|.. ArchivedState
DraftState ..> PublishedState : publish
PublishedState ..> ArchivedState : archive
合法状态转换如下;图中没有画出的动作均由当前实现拒绝。
stateDiagram-v2
[*] --> Draft
Draft --> Published : publish
Published --> Archived : archive
代码思路:变化应该落在哪个对象上
Manuscript 初始持有 DraftState。publish 与 archive 不再判断字符串状态,只调用 state 的对应方法。具体响应由当前运行时状态决定。
DraftState.publish 将上下文迁移到 PublishedState,而 DraftState.archive 直接抛异常;PublishedState 对应允许归档、拒绝重复发布;ArchivedState 拒绝这两个动作。迁移规则和行为靠在一起,阅读一个状态类就能看到当前状态允许做什么。
transitionTo 是包可见方法,对包外调用方不开放任意 setState。客户端通过业务动作推进生命周期,测试同时断言成功迁移和失败后的状态不变。包内代码仍需遵守边界,这不是不可绕过的安全控制。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.state,源码目录为 src/main/java/com/hanserwei/patterns/state/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
Manuscript.java:
package com.hanserwei.patterns.state;
import java.util.Objects;
/** 把生命周期动作交给当前状态对象的上下文. */
public final class Manuscript {
/** 当前状态,初始为草稿. */
private PublicationState state = new DraftState();
/** 请求发布,是否合法由当前状态决定. */
public void publish() {
state.publish(this);
}
/** 请求归档,是否合法由当前状态决定. */
public void archive() {
state.archive(this);
}
/** 返回当前状态名称. */
public String stateName() {
return state.name();
}
/** 供状态对象提交合法转换,不向包外暴露任意设置入口. */
void transitionTo(PublicationState state) {
this.state = Objects.requireNonNull(state, "state");
}
}
DraftState.java:
package com.hanserwei.patterns.state;
/** 封装一种文章状态的动作与合法迁移. */
public final class DraftState implements PublicationState {
/** 处理当前状态的发布请求;不允许时抛出 IllegalStateException. */
@Override
public void publish(Manuscript manuscript) {
manuscript.transitionTo(new PublishedState());
}
/** 处理当前状态的归档请求;不允许时抛出 IllegalStateException. */
@Override
public void archive(Manuscript manuscript) {
throw new IllegalStateException("Draft cannot be archived");
}
/** 返回状态标识. */
@Override
public String name() {
return "draft";
}
}
PublishedState.java:
package com.hanserwei.patterns.state;
/** 封装一种文章状态的动作与合法迁移. */
public final class PublishedState implements PublicationState {
/** 处理当前状态的发布请求;不允许时抛出 IllegalStateException. */
@Override
public void publish(Manuscript manuscript) {
throw new IllegalStateException("Already published");
}
/** 处理当前状态的归档请求;不允许时抛出 IllegalStateException. */
@Override
public void archive(Manuscript manuscript) {
manuscript.transitionTo(new ArchivedState());
}
/** 返回状态标识. */
@Override
public String name() {
return "published";
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.state;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
Manuscript manuscript = new Manuscript();
manuscript.publish();
System.out.println(manuscript.stateName());
manuscript.archive();
System.out.println(manuscript.stateName());
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runState
./gradlew test --tests 'com.hanserwei.patterns.state.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
published
archived
用测试确认模式的行为
合法路径是 draft → published → archived;草稿归档、重复发布、归档后发布和重复归档均失败,失败后状态保持原值。
对应测试位于 src/test/java/com/hanserwei/patterns/state/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 订单、工单、文章等对象具有明确生命周期且行为随状态变化。
- 连接协议、播放器或设备在不同状态接受不同操作。
- 状态分支分散在多个方法,维护时容易漏改。
适用边界与容易踩的坑
状态对象的可变字段需要特别小心。如果多个上下文共享一个状态实例,该状态应当无状态或只保存不可变元数据;否则一篇文章的数据可能进入另一篇。当前实现每次迁移创建新的无字段状态,便于学习所有权。
状态模式不自动保证并发转换原子性。两个线程同时调用 publish,或数据库里另一个请求已经归档,都需要同步、版本检查或事务支持。本例只验证内存中的顺序调用。
只有一个方法、三个简单分支时,enum 加 switch 往往已经足够清楚。状态类最适合状态相关行为有一定规模的场景;不要为一个布尔开关制造两套空类。
与相近模式比较
策略通常由调用方选择算法,策略之间没有固定生命周期;状态通常随上下文内部业务动作迁移,状态之间存在合法转换关系。接口相似,切换原因不同。
动手练习
新增待审核状态,规定草稿必须先提交审核,审核通过后才能发布。先画状态转换图并列出每个动作的非法组合,再实现类;检查新增状态是否要求对状态接口增加新动作。
系列导航:Java 25 设计模式学习指南