1 解决的问题

如果一个对象有多种状态,并且每种状态下的行为不同,一般的做法是在这个对象的各个行为中添加 if-else 或者 switch-case 语句。但更好的做法是为每种状态创建一个状态对象,使用状态对象替换掉这些条件判断语句,使得状态控制更加灵活,扩展性也更好。

2 状态模式

状态模式:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了自己的类一样。

通俗地说,状态模式就是一个关于多态的设计模式。把事件触发的 状态转移和动作执行,拆分到不同的状态类中,避免了分支判断结构。即状态模式与有限状态机的观念紧密相关。

3 实现代码

  • Context (上下文信息类) → 存储当前状态类,并负责具体状态的切换;
  • State (抽象状态类) → 定义声明状态更新的操作方法,可以是接口或抽象类;
  • ConcreteState (具体状态类) → 实现抽象状态类中定义的方法,根据具体场景指定对应状态改变后的代码逻辑;
// 抽象状态
public abstract class State {
    protected StateContext context;
    public void setContext(StateContext context) { this.context = context; }
    abstract void onHomeClick();
    abstract void onPowerClick();
    abstract void onVolumeAscClick();
    abstract void onVolumeDescClick();
}
 
 
// 具体状态 → 关机状态
public class CloseState extends State {
    @Override public void onHomeClick() { System.out.println("处于关机状态,按Home键没有反应"); }
    @Override void onPowerClick() {
        System.out.println("手机开机");
        context.setState(FirstBootState.class);
        context.setScreenOn(true);
        context.getState().onHomeClick();
    }
    @Override void onVolumeAscClick() { System.out.println("处于关机状态,按音量+没反应"); }
    @Override void onVolumeDescClick() {  System.out.println("处于关机状态,按音量-没反应"); }
}
 
// 具体状态 → 第一次启动状态
public class FirstBootState extends State {
    @Override public void onHomeClick() {
        System.out.println("首次启动,可以进行密码解锁");
        System.out.println("解锁完毕,进入主界面");
        context.setState(AfterBootState.class);
        context.setScreenOn(true);
    }
    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
        } else {
            System.out.println("亮屏,等待密码解锁");
        }
        context.setScreenOn(!context.isScreenOn());
    }
    @Override void onVolumeAscClick() { System.out.println("音量+"); }
    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}
 
// 具体状态 → 非第一次启动状态
public class NotFirstBootState extends State {
    @Override public void onHomeClick() {
        System.out.println("非首次启动,可以通过密码或指纹解锁");
        System.out.println("解锁完毕,进入主界面");
        context.setScreenOn(true);
        context.setState(AfterBootState.class);
    }
    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
        } else {
            System.out.println("亮屏,等待密码或指纹解锁");
            context.setState(NotFirstBootState.class);
        }
        context.setScreenOn(!context.isScreenOn());
    }
    @Override void onVolumeAscClick() { System.out.println("音量+"); }
    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}
 
// 具体状态 → 启动后
public class AfterBootState extends State {
    @Override void onHomeClick() { System.out.println("返回主界面"); }
    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
            context.setState(NotFirstBootState.class);
        } else {
            System.out.println("亮屏,等待密码或指纹解锁");
            context.getState().onHomeClick();
        }
        context.setScreenOn(!context.isScreenOn());
    }
    @Override void onVolumeAscClick() { System.out.println("音量+"); }
    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}
 
// 上下文信息类
public class StateContext {
    private boolean isScreenOn = false;   // 屏幕是否亮着
    public final static Map<Class, State> stateMap = new HashMap<>();
    private State state;    // 手机当前状态
    static {
        stateMap.put(CloseState.class, new CloseState());
        stateMap.put(FirstBootState.class, new FirstBootState());
        stateMap.put(NotFirstBootState.class, new NotFirstBootState());
        stateMap.put(AfterBootState.class, new AfterBootState());
    }
    public void setState(Class stateClass) {
        this.state = stateMap.get(stateClass);
        this.state.setContext(this);
    }
    public State getState() { return state; }
    public boolean isScreenOn() { return isScreenOn; }
    public void setScreenOn(boolean screenOn) {
        isScreenOn = screenOn;
        System.out.println("===> 屏幕处于:" + (isScreenOn ? "亮屏状态": "熄屏状态"));
    }
}
 
// 测试用例
public class StateTest {
    public static void main(String[] args) {
        StateContext context = new StateContext();
        context.setState(CloseState.class);
        // 处于关机状态点击音量- 和 home键
        context.getState().onVolumeDescClick();
        context.getState().onHomeClick();
        // 处于关机状态点击电源键
        context.getState().onPowerClick();
        context.getState().onPowerClick();
        context.getState().onHomeClick();
        context.getState().onVolumeAscClick();
    }
}
 

4 优缺点

优点

  • 单一职责原则:将与特定状态相关的代码组织到单独的类中;
  • 更好的扩展性:扩展新的状态只需增加实现类,在需要维护的地方设置下新状态即可;
  • 避免条件判断的状态机条件;

缺点

  • 类增加,每个状态对应一个具体状态类;
  • 不满足开闭原则。虽然增加新状态不用修改其他类,但是如果存在旧状态到新状态的状态切换联系时,还是需要修改其他类。
  • 逻辑零散,无法在一个地方就看出整个状态机的转换逻辑;

5 应用场景

  • 对象根据自身不同的状态对一个响应做出不同的反应,并且状态的数量比较多;
  • 某个类需要根据成员变量的当前值改变自身行为,从而需要大量条件判断时;

6 有限状态机的概念

英文翻译 Finite State Machine,缩写 FSM,简称状态机,它有三个组成部分:状态(State)事件(Event)动作(Action)。其中的事件又称为 转移条件,事件触发状态的转移和动作的执行(非必须)。

也可以理解为一种数学模型,该模型中有几个状态(有限的),在不同场景下,不同的状态间发生转移,在状态转移过程中可能伴随着不同的事件发生。

状态机有三种常见的实现方式:

  • 分支逻辑法 → 缺点是改变业务逻辑,改起来容易出错,代码也不易看懂。适合简单状态机;
  • 查表法 → 适用于状态很多、状态转移比较复杂的状态机,用二维数组表示状态转移图,可极大提高代码的可读性与可维护性;
  • 状态模式 → 适用于状态并不多、状态转移较简单,事件触发动作包含的业务逻辑可能较复杂的状态机。