State Pattern, 状态模式
不同的状态,不同的行为;或者说,每个状态有着相应的行为。
何时使用
State 模式适合"状态的切换"。因为我们经常会使用 if-elseif-else 进行状态切换,如果针对状态的判断切换反复出现,就要联想到是否可以采取 State 模式了。
不只是根据状态,也有根据属性。如果某个对象的属性不同,对象的行为就不一样,这点在数据库系统中出现频率比较高——经常会在数据表的尾部加上属性字段,用以标识记录中一些特殊性质,这种属性的切换又是随时可能发生的,就有可能要使用 State。
是否使用
“开关切换状态"和"一般的状态判断"有区别:
一般的状态判断:state 的值由另一个变量决定,二者没有关系。
if (which == 1) state = "hello";
else if (which == 2) state = "hi";
else if (which == 3) state = "bye";
开关切换状态:state 的下一个值由当前值决定,像旋转开关一样。
if (state.equals("bye")) state = "hello";
else if (state.equals("hello")) state = "hi";
else if (state.equals("hi")) state = "bye";
如果只有一个方向的切换,不一定需要 State 模式(建立很多子类会增加复杂性);但如果同一操作在多个状态下行为各不相同,且状态数量会增长,就应该考虑 State 了。
示例:未使用 State 模式
TCP 连接有三个核心状态:CLOSED、LISTEN、ESTABLISHED。对同一操作,不同状态的行为完全不同:
public class TCPConnection {
private String state = "CLOSED";
public void open() {
if (state.equals("CLOSED")) {
// 发送 SYN
state = "ESTABLISHED";
}
// LISTEN / ESTABLISHED 状态下忽略
}
public void close() {
if (state.equals("LISTEN")) {
state = "CLOSED";
} else if (state.equals("ESTABLISHED")) {
// 发送 FIN
state = "CLOSED";
}
// CLOSED 状态下忽略
}
public void acknowledge() {
if (state.equals("LISTEN")) {
state = "ESTABLISHED";
} else if (state.equals("ESTABLISHED")) {
// 处理数据 ACK
}
// CLOSED 状态下忽略
}
}
随着状态增多,if-else 链会急剧膨胀,且每次新增状态都要修改所有方法。下面用 State 模式重构。
如何使用
State 模式需要两种类型实体:
- State Manager(状态管理器):负责持有当前状态并委托操作(上例中的
TCPConnection)。 - 抽象状态父类/接口:不同状态是继承该父类的子类。
第一步:建立状态父类
public abstract class TCPState {
public void open(TCPConnection c) {}
public void close(TCPConnection c) {}
public void acknowledge(TCPConnection c) {}
}
父类提供空实现,子类只需覆写本状态下有意义的操作。
第二步:实现各状态子类
public class TCPClosed extends TCPState {
@Override
public void open(TCPConnection c) {
// 发送 SYN,进入已建立状态
c.setState(new TCPEstablished());
}
}
public class TCPListen extends TCPState {
@Override
public void close(TCPConnection c) {
c.setState(new TCPClosed());
}
@Override
public void acknowledge(TCPConnection c) {
// 收到 SYN,完成握手
c.setState(new TCPEstablished());
}
}
public class TCPEstablished extends TCPState {
@Override
public void close(TCPConnection c) {
// 发送 FIN,关闭连接
c.setState(new TCPClosed());
}
@Override
public void acknowledge(TCPConnection c) {
// 处理数据 ACK
}
}
第三步:改写 State Manager
public class TCPConnection {
private TCPState state = new TCPClosed();
public void setState(TCPState state) {
this.state = state;
}
public void open() { state.open(this); }
public void close() { state.close(this); }
public void acknowledge() { state.acknowledge(this); }
}
至此完成重构。调用方只需调用 open() / close() / acknowledge(),无需关心当前状态——状态自己决定如何响应并完成跳转。
每个状态类只负责两件事:在当前状态下如何响应某个动作,以及是否需要切换到下一个状态。其他状态的逻辑完全不需要关心。状态也可以选择什么都不做(不切换),比如 TCPClosed 收到 close() 时直接忽略——父类的空实现解决了"这个状态下这个操作没意义"的问题,不需要到处写 if (state == X) return;。
优点
- 封装转换过程(转换规则)。
- 枚举可能的状态,需要事先确定状态种类。
状态模式可以允许客户端改变状态的转换行为,而状态机能够自动改变状态。
应用场景
- 银行账户:在 Open 和 Close 状态间转换。
- TCP 连接:创建、侦听、关闭三个状态反复转换,每个状态的具体行为较复杂,适合使用 State。
- 邮箱 POP 账号:start、HaveUsername、Authorized、quit 四种状态。
- 绘图工具:用户选择不同工具(方框、直线、曲线)可看作状态切换。
- 工作流/政府 OA:批文有未办、正在办理、正在批示、正在审核、已完成等状态。
- 网络游戏:游戏活动有开始、开玩、正在玩、输赢等状态。
实质
使用状态模式前,客户端外界需要介入改变状态,而状态改变的实现是琐碎或复杂的。
使用状态模式后,客户端可以直接使用事件 Event 实现,无需关心该事件导致何种状态变化——这些由状态机等内部实现。
这是一种 Event-condition-State,状态模式封装了 condition-State 部分。
每个状态形成一个子类,每个状态只关心它的下一个可能状态,从而无形中形成了状态转换的规则。新增状态时,只需修改和定义其前一个状态。
状态转换的实现方法:
- 在每个状态中实现
next(),指定下一个状态。 - 设定一个 StateOwner,在其中定义
stateEnter和stateExit行为。
参考:设计模式之 State