【设计模式】行为型模式-状态模式
状态模式 / State
状态模式是一种行为设计模式,让你能在一个对象的内部状态变化的时候改变其行为,让它看上去就像改变了自身所属的类一样。
基本思想
状态模式与 有限状态机 紧密相关。状态模式的主要思想是,程序在任意时刻只能处于某几种有限的状态中。在任意一个特定状态下,程序的行为都不相同,且可以瞬间从一个状态切换到另一个状态。不过,根据当前状态,程序有可能切换到另一个状态,也可能保持当前状态不变。这些数量有限、且预先定义好的状态切换规则被称为 转移。
一个Java代码例子
Example
假如我们有一个文档
Document类,该类可能处于以下几种状态:
- 草稿
Draft- 审阅中
Moderation- 已发布
Published而针对这些不同的状态,文档的
publish()方法在不同状态下的行为略有不同:
状态 publish()方法的行为草稿 Draft将文档转移到【审阅中】状态 审阅中 Moderation若当前用户为管理员,则会公开发布文档 已发布 Published不进行任何操作 状态机通常由众多条件运算符 (
if或switch) 实现, 可根据对象的当前状态选择相应的行为。 比如下面的代码,根据文档的当前状态和用户的角色来更新文档的状态。public class Document { private String state; // 定义状态字段 // 构造函数,初始化文档状态 public Document(String initialState) { this.state = initialState; } // publish方法,根据当前状态更新文档状态 public void publish(String currentUserRole) { switch (state) { case "draft": state = "moderation"; break; case "moderation": if ("admin".equals(currentUserRole)) { state = "published"; } break; case "published": // 什么也不做 break; default: // 可以在这里处理未定义状态的情况 break; } } // 主函数,用于测试Document类 public static void main(String[] args) { Document doc = new Document("draft"); // 创建一个状态为草稿的文档 System.out.println("Before publish: " + doc.getState()); // 打印状态 doc.publish("admin"); // 模拟管理员调用发布方法 System.out.println("After publish: " + doc.getState()); // 打印状态 } }
存在问题
正如上小节所说,状态机通常由众多条件运算符(if 或 switch)实现,当我们逐步添加在一个类中添加更多的状态和依赖于状态的行为后,基于条件语句的状态机就会暴露其最大的问题:条件语句过于复杂,代码维护工作困难。
解决方案
状态模式建议为对象的所有可能状态新建一个类,然后将所有状态的对应行为抽取到这些类中。
原始对象被称为上下文(Context),它不会自行实现所有的行为,而是会保存一个指向表示当前状态的状态对象的引用,且将所有与状态相关的工作委派给该对象。

如刚才的「文档」例子并参见上图,此时 Document 类即为一个 context,它保存了一个指向当前状态的状态对象的引用(state)。
将刚才充斥着 if-else 的代码改写为状态模式:
-
首先,定义一个
State接口public interface State { void publish(Document document); } -
为每个状态创建具体的类
public class DraftState implements State { @Override public void publish(Document document) { document.setState(new ModerationState()); } } public class ModerationState implements State { @Override public void publish(Document document) { if (document.getCurrentUser().getRole().equals("admin")) { document.setState(new PublishedState()); } } } public class PublishedState implements State { @Override public void publish(Document document) { // 什么也不做。 } } -
修改
Document类以使用状态模式public class Document { private State state; private User currentUser; public Document() { this.state = new DraftState(); // 默认状态为草稿 } public void publish() { state.publish(this); } public void setState(State state) { this.state = state; } public State getState() { return state; } public User getCurrentUser() { return currentUser; } public void setCurrentUser(User currentUser) { this.currentUser = currentUser; } }
在这个重写的版本中,Document 类包含一个 State 类型的字段 state,它负责维护文档的状态。publish 方法现在调用 state 对象的 publish 方法,而不是直接在 Document 类中处理状态转换逻辑。每个状态类都实现了 State 接口,并在 publish 方法中定义了如何将文档从当前状态转换到下一个状态。
这样,Document 类的 publish 方法就不再依赖于复杂的条件逻辑,而是将这些逻辑委托给了状态对象,使得代码更加清晰和易于维护。
状态模式的结构

-
上下文 (Context) 保存了对于一个具体状态对象的引用, 并会将所有与该状态相关的工作委派给它。 上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。
-
状态 (State) 接口会声明特定于状态的方法。 这些方法应能被其他所有具体状态所理解, 因为你不希望某些状态所拥有的方法永远不会被调用。
-
具体状态 (Concrete States) 会自行实现特定于状态的方法。 为了避免多个状态中包含相似代码, 你可以提供一个封装有部分通用行为的中间抽象类。
状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。
-
上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。
[!IMPORTANT]
状态模式和策略模式
状态模式的结构可能看上去与策略模式相似, 但有一个关键性的不同——在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。
状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给「帮手」对象来改变其在不同情景下的行为。 策略 使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但 状态 模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
一个更加全面的例子
现在我们有一个播放器,播放器有如下几种状态:
ReadyStateLockedStatePlayingState
且这个播放器的每个状态有如下几种行为:
- 点击锁定:
clickLock() - 点击播放:
clickPlay() - 点击下一首:
clickNext() - 点击上一首:
clickPrevious()
我们可以把类图画出来:

我们用状态模式的思想来分析可以知道,播放器 AudioPlayer 类为上下文 context,它会指向状态类实例的引用。
-
定义上下文,即音频播放器类(
AudioPlayer),AudioPlayer还会维护指向状态类实例的引用class AudioPlayer { private State state; // ... (其他和播放器有关的field,如音量、当前歌曲、播放列表等,此处省略) public AudioPlayer() { this.state = new ReadyState(this); } public void changeState(State state) { this.state = state; } public void clickLock() { this.state.clickLock(); } public void clickPlahy() { this.state.clickPlay(); } public void clickNext() { this.state.clickNext(); } public void clickPrevious() { this.state.clickPrevious(); } } -
声明一个抽象类,用于让所有的具体状态类继承该类,并且所有的具体状态类都必须实现状态基类声明的方法;抽象状态类并提供反向引用,指向与状态相关的上下文对象。状态可以使用反向引用,将上下文转换为另一个状态。
abstract class State { protected AudioPlayer player; // 上下文将自身传递给状态构造函数,这可帮助状态在需要时获取一些有用的上下文数据。 public State(AudioPlayer player) { this.player = player; } public abstract void clickLock(); public abstract void clickPlay(); public abstract void clickNext(); public abstract void clickPrevious(); } -
具体状态会实现与上下文状态相关的多种行为。
class LockedState extends State { public LockedState(AudioPlayer player) { super(player); } @Override public void clickLock() { if (player.isPlaying()) { player.changeState(new PlayingState(player)); } else { player.changeState(new ReadyState(player)); } } @Override public void clickPlay() { // 已锁定,什么也不做。 } @Override public void clickNext() { // 已锁定,什么也不做。 } @Override public void clickPrevious() { // 已锁定,什么也不做。 } } // 它们还可在上下文中触发状态转换。 class ReadyState extends State { public ReadyState(AudioPlayer player) { super(player); } @Override public void clickLock() { player.changeState(new LockedState(player)); } @Override public void clickPlay() { player.startPlayback(); player.changeState(new PlayingState(player)); } @Override public void clickNext() { player.nextSong(); } @Override public void clickPrevious() { player.previousSong(); } } class PlayingState extends State { public PlayingState(AudioPlayer player) { super(player); } @Override public void clickLock() { player.changeState(new LockedState(player)); } @Override public void clickPlay() { player.stopPlayback(); player.changeState(new ReadyState(player)); } @Override public void clickNext() { // 这里需要一个事件对象来检测双击事件,但为了简化,我们假设有一个方法来检测双击 if (player.isDoubleClick()) { player.nextSong(); } else { player.fastForward(5); } } @Override public void clickPrevious() { // 这里需要一个事件对象来检测双击事件,但为了简化,我们假设有一个方法来检测双击 if (player.isDoubleClick()) { player.previousSong(); } else { player.rewind(5); } } }
反向引用
在设计模式中,特别是在状态模式(State Pattern)中,反向引用是指从状态对象到其上下文对象的引用。在状态模式中,上下文(Context)对象持有一个指向当前状态对象的引用,而状态对象反过来也需要引用上下文对象以便执行某些操作。
为什么需要反向引用?
- 状态转换:状态对象需要触发状态的转换,这通常涉及到修改上下文中的状态引用。
- 访问上下文的特定方法或数据:状态对象可能需要访问或修改上下文中的数据或调用其方法,以执行与状态相关的操作。
反向引用的实现
在状态模式中,通常在状态对象的构造函数中接收一个指向上下文对象的引用。这个引用在状态对象中被保存,并在需要时使用。
示例
假设有一个简单的状态模式实现,其中状态对象需要触发状态转换:
// 状态接口
interface State {
void handleRequest(Context context);
}
// 具体状态类
class ConcreteStateA implements State {
public void handleRequest(Context context) {
System.out.println("Handling request in ConcreteStateA");
// 状态转换
context.setState(new ConcreteStateB());
}
}
class ConcreteStateB implements State {
public void handleRequest(Context context) {
System.out.println("Handling request in ConcreteStateB");
// 状态转换
context.setState(new ConcreteStateA());
}
}
// 上下文类
class Context {
private State state;
public Context(State state) {
this.state = state;
}
public void setState(State state) {
this.state = state;
}
public void request() {
state.handleRequest(this);
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Context context = new Context(new ConcreteStateA());
context.request(); // 将输出:Handling request in ConcreteStateA
context.request(); // 将输出:Handling request in ConcreteStateB
}
}
在这个例子中,State接口定义了一个handleRequest方法,该方法接收一个Context对象。每个具体的状态类实现这个接口,并在需要时通过调用setState方法来改变上下文的状态。这里的Context对象在状态对象中作为参数传递,允许状态对象访问和修改上下文的状态。
总结
反向引用是状态模式中的一个重要概念,它允许状态对象与上下文对象进行交互,包括访问上下文的数据和方法,以及触发状态的转换。这种设计使得状态转换逻辑更加集中和明确,有助于保持代码的清晰和可维护性。
浙公网安备 33010602011771号