Java 中状态机模式的两种实现方式:从 if/else 到显式规则管理

晚来风
晚来风
发布时间:2026-08-23| 39人浏览过
原创

前言

状态机在 Java 业务代码里并不少见,订单、审批、签章、同步流程几乎都会遇到。问题往往不在“有没有状态”,而在状态规则是散落在各个方法里,还是被集中管理起来;这篇文章就用订单流转示例拆开两种实现方式,并补充守卫条件、回退、复合状态这些实际开发里最容易踩坑的点,帮助你判断哪种做法更适合当前系统。

状态机这个词听起来偏理论,但放到业务系统里,其实就是在回答一个很具体的问题:一个对象当前处于什么状态、什么操作能触发变化、变化之后又该执行哪些动作。以订单、审批单、签章流程这类对象为例,只要生命周期里存在多个阶段,状态机思路几乎都会出现。

这篇文章就围绕 Java 中最常见的两种实现方式展开:一种是把状态判断写进业务代码,另一种是把流转规则抽出来集中管理。看完之后,你可以据此判断自己的场景是继续保持轻量实现,还是该尽早切到显式状态机,避免后期规则失控。

什么是状态机

状态机本质上是一个模型,用来描述对象在生命周期内如何从一个状态走到另一个状态。放到代码里理解,可以抓住三个核心元素:

  • 状态(State):对象当前所处的阶段,比如订单是“待支付”还是“已发货”。
  • 事件(Event):触发变化的动作或条件,比如用户点击“支付”。
  • 转换(Transition):在某个状态下,遇到某个事件后,流转到目标状态。

可以把它理解成一句固定句式:对象在某个状态下,因为某个事件,转换到了另一个状态。

Java 中两种常见实现方式

显式状态机

显式状态机会把状态和转换规则单独抽出来管理,通常有清晰的转换表、转换图,或者直接使用框架,例如 Spring StateMachine。它的重点在于:规则集中、边界清楚、非法流转统一拦截。

这种做法通常有几个特点:

  • 状态和转换规则集中配置
  • 存在明确的状态转换表或流程图
  • 非法转换可以统一校验和拦截
  • 更适合状态多、规则复杂、变更频繁的业务

隐式状态机

隐式状态机则更常见于日常业务开发。状态值通常只是数据库中的一个字段,具体能不能流转,全靠各个业务方法里的 if/else 或 switch 去判断。

它的特点也很明显:

  • 状态只是一个普通字段
  • 流转逻辑分散在各个方法中
  • 没有统一的规则中心
  • 实现简单,但随着业务变复杂,维护成本会快速上升

两者的差别,不在于有没有“状态”这个概念,而在于状态规则是否被集中管理。

隐式状态机怎么写

这是很多业务系统最常见的形态。下面用订单状态举例说明。

订单隐式状态机的流转路径与分散校验点示意图
隐式状态机的流转与校验分布用订单示例展示隐式状态机的状态字段、业务方法校验点和主要流转路径。

数据库中的状态字段

CREATE TABLE orders (
    id INT PRIMARY KEY,
    status INT NOT NULL DEFAULT 0 COMMENT '0=待支付, 1=已支付, 2=已发货, 3=已完成, 4=已取消'
);

这里的 status 就承担了状态机里的“状态”角色。

用枚举描述状态

public enum OrderStatus {
    UNPAID(0, "待支付"),
    PAID(1, "已支付"),
    SHIPPED(2, "已发货"),
    COMPLETED(3, "已完成"),
    CANCELLED(4, "已取消");

    private Integer code;
    private String desc;
}

即便是隐式状态机,状态枚举最好也要定义清楚。这样至少能避免到处写魔法值。

业务方法里直接做状态判断

public void payOrder(Integer orderId) {
    Order order = orderRepository.findById(orderId);
    // 隐式校验:只有待支付才能支付
    if (!OrderStatus.UNPAID.getCode().equals(order.getStatus())) {
        throw new BusinessException("当前状态不允许支付");
    }
    order.setStatus(OrderStatus.PAID.getCode());
    orderRepository.save(order);
}

public void shipOrder(Integer orderId) {
    Order order = orderRepository.findById(orderId);
    // 隐式校验:只有已支付才能发货
    if (!OrderStatus.PAID.getCode().equals(order.getStatus())) {
        throw new BusinessException("当前状态不允许发货");
    }
    order.setStatus(OrderStatus.SHIPPED.getCode());
    orderRepository.save(order);
}

public void cancelOrder(Integer orderId) {
    Order order = orderRepository.findById(orderId);
    // 隐式校验:已发货和已完成不能取消
    if (OrderStatus.SHIPPED.getCode().equals(order.getStatus())
        || OrderStatus.COMPLETED.getCode().equals(order.getStatus())) {
        throw new BusinessException("当前状态不允许取消");
    }
    order.setStatus(OrderStatus.CANCELLED.getCode());
    orderRepository.save(order);
}

对应的流转关系可以概括为:

UNPAID(0) ──[支付]──→ PAID(1) ──[发货]──→ SHIPPED(2) ──[确认收货]──→ COMPLETED(3)

│         │

└──[取消]──→ CANCELLED(4) ←──[取消]──┘

这种写法的优点是直接、上手快,尤其适合状态很少的场景。但它也有明显问题:规则散落在多个方法里,新增状态或修改流转时,很容易漏改某个分支。

显式状态机怎么实现

如果业务状态开始变多,或者流转规则经常调整,更稳妥的做法是把规则抽出来,形成统一入口。

显式状态机两种实现方式及适用场景对比图
显式状态机的两种写法把转换表和枚举方法两种显式实现放在同一张图里,并补充与隐式实现的选择依据。

方式一:状态转换表

最直接的方式,是用一张转换表集中声明“当前状态 + 事件 = 目标状态”。

public class OrderStateMachine {

    // 转换规则表:Map<当前状态, Map<事件, 目标状态>>
    private static final Map<Integer, Map<String, Integer>> TRANSITIONS = new HashMap<>();

    static {
        // 待支付状态下的合法转换
        Map<String, Integer> unpaidTransitions = new HashMap<>();
        unpaidTransitions.put("PAY", 1);       // 支付 → 已支付
        unpaidTransitions.put("CANCEL", 4);    // 取消 → 已取消
        TRANSITIONS.put(0, unpaidTransitions);

        // 已支付状态下的合法转换
        Map<String, Integer> paidTransitions = new HashMap<>();
        paidTransitions.put("SHIP", 2);        // 发货 → 已发货
        paidTransitions.put("CANCEL", 4);      // 取消 → 已取消
        TRANSITIONS.put(1, paidTransitions);

        // 已发货状态下的合法转换
        Map<String, Integer> shippedTransitions = new HashMap<>();
        shippedTransitions.put("CONFIRM", 3);  // 确认 → 已完成
        TRANSITIONS.put(2, shippedTransitions);
    }

    /**
     * 执行状态转换.
     */
    public static Integer transition(Integer currentState, String event) {
        Map<String, Integer> allowed = TRANSITIONS.get(currentState);
        if (allowed == null || !allowed.containsKey(event)) {
            throw new IllegalStateException(
                "非法状态转换: 状态=" + currentState + ", 事件=" + event);
        }
        return allowed.get(event);
    }
}

业务代码就可以简化成统一调用状态机:

public void payOrder(Integer orderId) {
    Order order = orderRepository.findById(orderId);
    // 由状态机统一校验和转换
    Integer newStatus = OrderStateMachine.transition(order.getStatus(), "PAY");
    order.setStatus(newStatus);
    orderRepository.save(order);
}

这种方式的价值在于,所有合法流转都能在一个地方看清楚,排查问题和扩展规则都会轻松很多。

方式二:枚举 + 方法

如果你希望状态本身就具备行为,也可以把转换逻辑直接写进枚举中。

public enum OrderStatus {
    UNPAID(0) {
        @Override
        public OrderStatus onPay() { return PAID; }
        @Override
        public OrderStatus onCancel() { return CANCELLED; }
    },
    PAID(1) {
        @Override
        public OrderStatus onShip() { return SHIPPED; }
        @Override
        public OrderStatus onCancel() { return CANCELLED; }
    },
    SHIPPED(2) {
        @Override
        public OrderStatus onConfirm() { return COMPLETED; }
    },
    COMPLETED(3),
    CANCELLED(4);

    private Integer code;

    // 默认实现:抛异常表示不允许该操作
    public OrderStatus onPay() { throw new IllegalStateException("当前状态不允许支付"); }
    public OrderStatus onShip() { throw new IllegalStateException("当前状态不允许发货"); }
    public OrderStatus onConfirm() { throw new IllegalStateException("当前状态不允许确认"); }
    public OrderStatus onCancel() { throw new IllegalStateException("当前状态不允许取消"); }
}

这种写法的好处是每个状态自己知道允许哪些操作,调用方不必关心完整的判断过程。相比转换表,它更偏面向对象;相比隐式判断,它的规则仍然是集中可见的。

隐式与显式状态机怎么选

维度隐式状态机显式状态机
实现成本低,直接写 if/else中等,需要定义转换规则
可读性差,状态规则分散在各方法中好,规则集中一目了然
维护成本状态少时低,状态多时急剧上升稳定,新增状态只需加规则
安全性容易遗漏校验导致非法转换统一拦截非法转换
适用场景状态少(3-5个)且变化少状态多或流转规则复杂

经验上看,如果只有两三个状态,且业务基本稳定,隐式状态机完全可以接受;但一旦状态超过 5 个,或者流程里开始出现取消、回退、补偿、外部系统联动,继续把规则散在业务方法里,后面大概率会越改越乱。

状态机落地时还要考虑什么

真正上线的业务流转,往往不只是“状态变一下”这么简单。下面几个概念经常会一起出现。

状态机高级概念与复合状态设计要点图
状态机落地时的四个关键问题概括守卫条件、转换动作、回退以及多字段复合状态的关系,适合放在进阶部分帮助读者建立完整设计视角。

守卫条件(Guard)

事件发生后,不一定就能直接流转,往往还要做额外校验。比如订单发货前必须确认库存充足:

// 事件是"发货",但还需要守卫条件:库存充足
public Integer transition(Integer currentState, String event, Order order) {
    if ("SHIP".equals(event) && order.getStock() <= 0) {
        throw new BusinessException("库存不足,无法发货");
    }
    return TRANSITIONS.get(currentState).get(event);
}

守卫条件的意义,是把“能不能转”这件事判断完整,而不是只看事件名。

转换动作(Action)

状态转换完成时,通常还会伴随业务动作,比如取消订单时自动退款、通知用户:

// 转换到"已取消"时,自动执行退款
public void cancelOrder(Order order) {
    Integer newStatus = OrderStateMachine.transition(order.getStatus(), "CANCEL");
    order.setStatus(newStatus);
    // 转换动作
    refundService.refund(order.getPaymentId());
    notificationService.notifyUser(order.getUserId(), "订单已取消");
}

这里要关注的是,状态变化和业务动作通常不能割裂处理,否则很容易出现“状态改了,但后续动作没执行完整”的问题。

入口动作 / 出口动作(Entry/Exit Action)

有些动作不是跟事件绑定,而是跟“进入某个状态”绑定。比如无论从哪个状态进入“已取消”,都要发通知并释放库存:

// 无论从哪里进入"已取消"状态,都发通知
private void onEnterCancelled(Order order) {
    notificationService.notifyUser(order.getUserId(), "订单已取消");
    inventoryService.releaseStock(order.getItems());
}

这种场景适合把公共处理抽成状态入口动作,避免多处重复。

状态回退(Rollback)

有些业务还需要逆向流转。比如物流拦截成功后,订单可能要从“已发货”回退到“已支付”:

// 从已发货回退到已支付(例如物流拦截成功)
public void rollbackShipment(Order order) {
    if (!OrderStatus.SHIPPED.getCode().equals(order.getStatus())) {
        throw new BusinessException("只有已发货状态才能回退");
    }
    order.setStatus(OrderStatus.PAID.getCode());
    // 撤销动作:取消物流单、恢复库存
    logisticsService.cancelShipment(order.getLogisticsNo());
    inventoryService.restoreStock(order.getItems());
}

回退最容易被低估。因为它不只是把状态字段改回去,还要考虑之前的副作用是否也要补偿或撤销。

多字段联动时的复合状态问题

实际系统里,一个对象经常不止一个状态字段。比如订单既有签章状态,也有同步状态,这时就会形成复合状态:

// 两个独立的状态维度
private Integer signStatus;    // 签章状态
private Integer syncYcStatus;  // 同步状态

// 复合状态:只有签章完成且已同步才算"流程结束"
// 回退时两个维度可能都需要回退

这类场景需要额外看清楚三件事:

  • 两个状态维度之间是否存在依赖关系,例如同步必须在签章完成后进行
  • 回退是否需要联动处理,一个维度回退后另一个维度是否也要跟着回退
  • 外部系统是否已经接收数据,如果涉及第三方不可逆操作,回退策略必须更谨慎

单字段状态机和复合状态机的复杂度差别很大。前者主要处理流转规则,后者还要处理维度间约束和补偿策略。

总结

状态机要解决的核心问题,其实就是对象生命周期管理。无论你最终选隐式还是显式实现,都绕不开下面几件事:

  1. 先把状态定义完整,最好用枚举统一表达
  2. 把事件和转换规则梳理清楚,避免规则散落在多个方法中
  3. 把转换时需要执行的动作一起设计进去
  4. 对非法转换建立统一的校验和拦截机制
  5. 提前考虑回退、补偿,以及多状态字段联动场景

如果你的业务状态少、变化也少,隐式状态机足够实用;但只要流程开始变长、规则开始变多,尽早收拢成显式状态机,通常会比后期补洞更省成本。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。