状态模式:状态转换、有限状态机

状态模式,说白了就是让对象的行为随着内部状态的改变而改变。你想想看,一个对象在不同状态下,对同一个请求的反应可能完全不同。比如一个订单,在“待支付”状态下,你调用“取消”方法,它会取消订单;但在“已发货”状态下,同样的“取消”方法,可能就变成申请退货了。

我刚开始接触设计模式时,觉得状态模式跟策略模式长得挺像。后来在项目中踩过坑才明白,这俩本质上是两码事。策略模式是让你在多个算法之间自由切换,而状态模式是让对象的状态驱动行为变化,状态之间是有流转关系的。

什么是状态模式

状态模式的核心思想其实很简单:把每个状态封装成一个独立的类,让每个状态类负责处理自己状态下的行为。这样当对象的状态发生变化时,只需要切换当前的状态对象就行了。

我记得有一次重构一个工作流引擎,里面有个审批节点,状态有“待审批”、“审批中”、“已通过”、“已驳回”四种。一开始代码里全是 if-else 判断,看得我头皮发麻。后来用状态模式一重构,整个世界都清净了。

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

状态模式的结构

状态模式通常包含三个角色:

  • Context(上下文):持有当前状态对象的引用,负责状态的切换
  • State(抽象状态):定义状态相关的行为接口
  • ConcreteState(具体状态):实现具体状态下的行为逻辑

来看一个我实际项目中简化过的例子——电梯控制系统。电梯有开门、关门、运行、停止四种状态。不同状态下,按同一个按钮效果完全不同。

// 抽象状态
public interface ElevatorState {
    void openDoor();
    void closeDoor();
    void run();
    void stop();
}

// 具体状态:开门状态
public class OpenState implements ElevatorState {
    @Override
    public void openDoor() {
        System.out.println("门已经是开着的");
    }

    @Override
    public void closeDoor() {
        System.out.println("门正在关闭...");
        // 切换到关门状态
    }

    @Override
    public void run() {
        System.out.println("门开着不能运行");
    }

    @Override
    public void stop() {
        System.out.println("已经停着");
    }
}

// 具体状态:运行状态
public class RunState implements ElevatorState {
    @Override
    public void openDoor() {
        System.out.println("运行中不能开门");
    }

    @Override
    public void closeDoor() {
        System.out.println("门已经是关着的");
    }

    @Override
    public void run() {
        System.out.println("已经在运行");
    }

    @Override
    public void stop() {
        System.out.println("正在停止...");
        // 切换到停止状态
    }
}

// 上下文
public class Elevator {
    private ElevatorState currentState;

    public Elevator() {
        this.currentState = new StopState(); // 初始状态:停止
    }

    public void setState(ElevatorState state) {
        this.currentState = state;
    }

    public void openDoor() {
        currentState.openDoor();
    }

    public void closeDoor() {
        currentState.closeDoor();
    }

    public void run() {
        currentState.run();
    }

    public void stop() {
        currentState.stop();
    }
}

你看,每个状态类只关心自己状态下的行为逻辑。如果某个操作在当前状态下不允许,直接拒绝就行。这样代码的可读性和可维护性都上去了。

有限状态机(FSM)

说到状态模式,就不得不提有限状态机。有限状态机是一种数学模型,它由有限个状态、有限个输入事件、状态转移规则和初始状态组成。状态模式其实就是有限状态机在面向对象编程中的一种实现方式。

我做过一个游戏开发项目,里面的人物角色有“站立”、“行走”、“奔跑”、“跳跃”、“攻击”等状态。这些状态之间的转换是有严格规则的——你不能从“站立”直接跳到“攻击”,必须先切换到“攻击准备”状态。这种场景用有限状态机来管理再合适不过了。

我的经验:在实现有限状态机时,我习惯把状态转移表单独抽出来管理。这样状态和转移逻辑解耦,后期加新状态或者改转移规则,都不需要动状态类本身。

状态转移图

下面我用 SVG 画一个简单的电梯状态转移图,帮你直观理解状态之间的流转关系:

电梯状态转移图 停止 StopState 运行 RunState 开门 OpenState 关门 CloseState 按运行键 按停止键 按开门键 按关门键 关门完成 按运行键 说明:每个状态只能通过特定事件转移到下一个状态,非法操作会被状态类拒绝

状态模式 vs 策略模式

这个问题我面试时经常被问到。很多人分不清状态模式和策略模式,其实它们有本质区别:

对比维度 状态模式 策略模式
核心目的 管理对象的状态流转 封装可互换的算法
状态关系 状态之间有转移规则 策略之间相互独立
行为选择 由当前状态决定 由客户端选择
状态数量 通常固定且有限 可以动态增加
典型场景 工作流、游戏角色 排序算法、支付方式

说白了,策略模式是你主动选算法,状态模式是状态替你做了选择。你想想看,在策略模式里,客户端需要知道有哪些策略,然后自己选一个。但在状态模式里,客户端根本不需要关心当前是什么状态,它只管调用方法,状态对象自己会处理。

实际项目中的应用

我在做电商系统时,订单状态管理就是一个典型的状态模式应用。订单有“待支付”、“已支付”、“已发货”、“已收货”、“已完成”、“已取消”等状态。每个状态下,用户能做的操作完全不同。

避坑指南:我曾经在一个项目中,把状态转移逻辑写在了上下文类里,结果状态类越来越多,上下文类也越来越臃肿。后来我改成把状态转移规则放在状态类自身,每个状态类都知道自己可以转移到哪些状态,这样上下文类就轻量多了。

还有一个常见的坑是状态爆炸。如果状态太多,状态类也会很多。我的建议是:状态数量控制在 5-7 个以内,超过这个数就要考虑用状态表或者状态机框架来管理了。

状态模式的优缺点

优点很明显:

  • 把状态相关的行为集中到同一个类中,符合单一职责原则
  • 新增状态很方便,只需要新增一个状态类
  • 消除了大量的 if-else 或 switch-case 判断

缺点也不容忽视:

  • 状态类会增多,如果状态太多会显得臃肿
  • 状态转移逻辑分散在各个状态类中,不够集中
  • 如果状态转移规则经常变化,维护成本会上升

我的建议:如果你发现状态转移规则特别复杂,可以考虑引入状态机框架,比如 Spring StateMachine 或者 Squirrel。这些框架帮你管理状态转移表,你只需要关注业务逻辑就行。

嗯,状态模式就讲到这里。记住一句话:状态模式的核心不是状态本身,而是状态之间的流转规则。搞清楚了这一点,你就能用好这个模式了。


公众号:蓝海资料掘金营,微信deep3321