简介
状态是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
问题
状态模式与有限状态机的概念紧密相关。
有限状态机的主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个状态中, 程序的行为都不相同, 并且可以瞬间从一个状态切换到 另一个状态。 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则就是状态转移。
你可以把这个方法应用在对象上。 假如你有一个 文档Document类。 文档可能会处于草稿Draft 、 审阅中Moderation和已发布Published三种状态中的一种。 文档的 publish发布方法在不同状态下的行为略有不同:
- 处于草稿状态时, 发布方法会把状态转移到审阅中。
- 处于审阅中状态时, 如果当前用户是管理员, 发布方法会公开发布文档。
- 处于已发布状态时, 它不会进行任何操作。
状态机通常由众多条件运算符 ( if或 switch ) 实现, 可根据对象的当前状态选择相应的行为。 即使你之前从未听说过有限状态机, 你也很可能已经实现过状态模式。 下面的代码应该能帮助你回忆起来。
import java.util.Objects;
class Document {
private String state = "draft"; // 初始状态为草稿
private User currentUser; // 伪代码中的currentUser需要具体实现
public void publish() {
switch (state) { // 基于状态的条件分支
case "draft":
state = "moderation";
System.out.println("文档进入审核状态");
break;
case "moderation":
if (currentUser != null && "admin".equals(currentUser.getRole())) {
state = "published";
System.out.println("文档正式发布");
}
break;
case "published":
System.out.println("文档已发布,无需操作");
break;
default:
throw new IllegalStateException("非法状态: " + state);
}
}
// User相关类需要自行实现
static class User {
private final String role;
public User(String role) {
this.role = role;
}
public String getRole() {
return role;
}
}
// 设置当前用户
public void setCurrentUser(User user) {
this.currentUser = user;
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Document doc = new Document();
// 管理员操作
doc.setCurrentUser(new Document.User("admin"));
doc.publish(); // Draft → Moderation
doc.publish(); // Moderation → Published
// 普通用户操作
doc.setCurrentUser(new Document.User("user"));
doc.publish(); // 保持Published状态
}
}
当我们逐步在文档类中添加更多状态和依赖于状态的行为后, 基于条件语句的状态机就会出问题。 为了能根据当前状态选择相应行为, 绝大部分方法里都会包含复杂的条件语句。 修改转换逻辑可能会涉及到修改所有相关方法中的状态条件语句, 加大代码的维护难度。
这个问题会随着项目进行变得越发严重。 因为我们很难在设计阶段预测到所有可能的状态和转换。 随着时间推移, 最初只包含有限条件语句的简洁状态机可能会变得非常臃肿。
解决
状态模式建议为所有可能状态都新建一个对应的类,然后把该状态的对应行为抽取到类中。
原始对象被称为上下文(context),它并不会自行实现所有行为,而是会保存一个指向当前状态对象的引用,把所有跟状态相关的工作委派给这个对象。
如果需要把上下文转换成另外一种状态,就需要把当前引用的状态对象替换成另外一个新状态对象。当然采用这种方式是有前提的:所有状态类都必须遵循同样的接口,而且上下文必须只通过接口与这些状态对象进行交互。
这个结构可能看上去跟策略模式有点像,但有一个关键性的不同——在状态模式里,特定状态知道其他所有状态的存在,能触发从一个状态到另一个状态的转换;而策略几乎完全不知道其他策略的存在。
代码
// Context 类
class Document {
private DocumentState currentState;
private String currentUserRole;
public Document() {
this.currentState = new DraftState(); // 初始化为草稿状态
}
// 设置当前用户角色
public void setCurrentUserRole(String role) {
this.currentUserRole = role;
}
// 状态转移方法
void transitionTo(DocumentState state) { // 支持状态自我转换
this.currentState = state;
}
// 发布入口方法
public void publish() {
currentState.handlePublish(this); // 委托给当前状态处理
}
}
// State 接口
interface DocumentState {
void handlePublish(Document context); // 参数传递上下文引用
}
// 具体状态实现
class DraftState implements DocumentState {
@Override
public void handlePublish(Document doc) { // 草稿状态转审核
System.out.println("提交文档进入审核状态");
doc.transitionTo(new ModerationState()); // 状态自我转换
}
}
class ModerationState implements DocumentState {
@Override
public void handlePublish(Document doc) { // 根据管理员权限处理
if ("admin".equals(doc.currentUserRole)) { // 获取上下文中的角色信息
System.out.println("管理员发布文档");
doc.transitionTo(new PublishedState());
} else {
System.out.println("非管理员无法发布");
}
}
}
class PublishedState implements DocumentState {
@Override
public void handlePublish(Document doc) { // 终态不处理
System.out.println("文档已发布,无法重复操作");
}
}
// Client(客户端)
public class Client {
public static void main(String[] args) {
Document doc = new Document();
// 测试普通用户场景
doc.setCurrentUserRole("user");
doc.publish(); // Draft → Moderation
doc.publish(); // 保持Moderation
// 测试管理员场景
doc.setCurrentUserRole("admin");
doc.publish(); // Moderation → Published
// 检查终态稳定性
doc.publish(); // 保持Published状态
}
}
关键优化点
- 状态自管理:每个具体状态类自行控制下一步状态转换
- 上下文访问:通过传递文档引用获取当前用户角色(简化私有字段访问)
- 可扩展性:新增状态只需创建类并实现接口,无需修改现有代码
- 消除条件逻辑:完全移除switch/case结构,使用多态分发
总结
- 上下文(Context)保存了对于一个具体状态对象的引用,会把所有跟这个状态相关的工作委派给它。上下文通过状态接口和状态对象交互,并且会提供一个设置器用于传递新的状态对象。
- 状态(State)接口会声明特定于状态的方法。这些方法能被其他所有具体状态所理解及调用。
- 具体状态(Concrete States)会自行实现特定于状态的方法。为了避免多个状态中包含相似代码,你可以提供一个封装有部分通用行为的中间抽象类。
另外,状态对象可存储对于上下文对象的反向引用。状态可以通过这个引用从上下文里获取所需信息,并且能触发状态转移。 - 上下文和具体状态都可以设置上下文的下个状态,并可以通过替换连接到上下文的状态对象来完成实际的状态转换。