Demo 地址: https://github.com/ooblee/HelloDesignPattern
1. 定义
状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
现实的模式,比如人的情绪有状态,喜怒哀乐等等,不同的状态会随着某个事件而改变。
状态模式应用在这样的对象上面:
- 有多种状态。
- 不同的状态可以相互转换。
- 不同的状态拥有不同的行为和数据。
状态模式把每一个状态进行封装,单独一个状态类,然后把和该状态的操作内聚到一起,以及处理下一个状态的切换。
所以对于状态节点,需要关心的是:
- 如何对当前状态进行处理。
- 如何切换到其他状态。
2. 设计
状态模式主要有这三种元素:
- 环境类,对应有状态变化的对象,提供命令方法让外部来调用,进行状态的处理和切换。
- 抽象状态类,定义了状态的处理方法。
- 具体状态类,实现了状态的处理方法。
类图如下:
这里演示一个简单的模型。
假设我们的对象有三种状态:A,B 和 C。需要处理两个命令 cmd1 和 cmd2。对应的状态图为:
命令用枚举表示:
public enum Cmd {
CMD_1("cmd1"),
CMD_2("cmd2");
final String value;
Cmd(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
设计抽象状态类,处理命令的方法为 handle
,因为需要转移外部环境的状态,或者外部环境的业务方法,这里要传入环境类。
public interface IState {
void handle(Context context, Cmd cmd);
}
设计环境类,也就是需要状态变化的类,提供了三种方法
- 初始化方法
init
。 - 响应命令
cmd1
。 - 响应命令
cmd2
。
初始化方法把状态初始化为起始状态 A。然后在处理命令的方法中,调用了当前状态的 handle
进行处理。
public class Context {
private IState state;
public void init() {
state = new StateA();
}
public void switchState(IState state) {
this.state = state;
}
public void cmd1() {
state.handle(this, Cmd.CMD_1);
}
public void cmd2() {
state.handle(this, Cmd.CMD_2);
}
}
具体的状态类。
状态类 A,从状态图中可以看到, A 可以响应 cmd1 和 cmd2,所以对这两个命令都进行了处理。处理完毕后转移 Context 的状态。
public class StateA implements IState {
public void handle(Context context, Cmd cmd) {
switch (cmd) {
case CMD_1:
handleCmd1(context);
break;
case CMD_2:
handleCmd2(context);
break;
default:
System.out.println("State A 不处理 " + cmd.getValue());
break;
}
}
private void handleCmd1(Context context) {
System.out.println("State A 处理 cmd1");
context.switchState(new StateB());
System.out.println("A => B");
}
private void handleCmd2(Context context) {
System.out.println("State A 处理 cmd2");
context.switchState(new StateC());
System.out.println("A => C");
}
}
状态 B 只关心命令 cmd1,需要处理 cmd1:
public class StateB implements IState {
public void handle(Context context, Cmd cmd) {
switch (cmd) {
case CMD_1:
handleCmd1(context);
break;
default:
System.out.println("State B 不处理 " + cmd.getValue());
break;
}
}
private void handleCmd1(Context context) {
System.out.println("State B 处理 cmd1");
context.switchState(new StateC());
System.out.println("B => C");
}
}
状态 C 只关心命令 cmd2,需要处理 cmd2:
public class StateC implements IState {
public void handle(Context context, Cmd cmd) {
switch (cmd) {
case CMD_2:
handleCmd2(context);
break;
default:
System.out.println("State C 不处理 " + cmd.getValue());
break;
}
}
private void handleCmd2(Context context) {
System.out.println("State C 处理 cmd2");
context.switchState(new StateB());
System.out.println("C => B");
}
}
使用者,只需要创建一个环境类,调用初始化方法,然后任意发送命令。环境类就按照状态机正常地运转起来,无效的命令直接略过。
public class TestState {
public static void main(String[] args) {
// 初始化环境
Context context = new Context();
context.init();
// 处理命令
context.cmd1();
context.cmd1();
context.cmd2();
context.cmd2();
context.cmd1();
context.cmd2();
context.cmd2();
context.cmd2();
context.cmd2();
context.cmd2();
}
}
显示:
State A 处理 cmd1
A => B
State B 处理 cmd1
B => C
State C 处理 cmd2
C => B
State B 不处理 cmd2
State B 处理 cmd1
B => C
State C 处理 cmd2
C => B
State B 不处理 cmd2
State B 不处理 cmd2
State B 不处理 cmd2
State B 不处理 cmd2
后续的修改也很方便:
- 假如 A 状态要修改,只需要修改 StateA,不影响其他状态类。
- 新增状态,新增一个状态类,然后修改一下状态的切换方式,不影响其他的状态类。
3. 应用
一个对象内部状态变化,最简单的做法直接用 if else 条件选择,然后在选择代码块中进行处理。
当内部状态切换复杂的情况下,会产生大量的 if else 条件选择,修改不方便,可读性也大幅度降低。
这时候可以使用状态模式进行优化。这里它类似于策略模式,把内部各个状态需要处理的业务独立且内聚到一个类中,然后只需要关心当前状态的业务处理。
状态存在于内存,适用客户端的场景。
服务端开发中,需要对状态进行存储,可以选用数据库进行存储。使用状态模式可以实现一个简单的工作流引擎。
4. 特点
4.1. 优势
- 易修改:取代多重条件选择,修改状态不需要修改多个地方,只要修改当前状态的代码。
- 易扩展:新增状态创建新的状态类,新增到其他状态的切换代码。
- 易切换:状态相关代码内部到状态类中,每次转换只需要关心当前状态到其他状态的切换。
- 节约内存:状态类可以做成单例,全局共享。
4.2. 缺点
- 类数量膨胀。如果状态很多的话,意味着状态类也会相应增加。
- 系统变复杂。需要对着状态图看才容易理解整个流程。