news 2026/9/30 1:28:04

状态模式实战:订单状态流转、if-else重构与并发持久化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
状态模式实战:订单状态流转、if-else重构与并发持久化

第一次接手一个订单模块的改造需求时,我在 Service 层看到过这样一段代码:if (order.getStatus() == 1) { ... } else if (order.getStatus() == 2) { ... },一路嵌套到第七层,最后还有个else里写着throw new RuntimeException("状态异常")。改一个"已发货能否取消"的规则,我得同时打开三个类,找到十几处魔法数字。那之后我把整套逻辑用状态模式重构了一遍,代码量从 800 行降到 300 行左右,新增一个"部分退款"状态只动了两个文件。这篇文章就把状态模式从头到尾捋清楚:它到底把什么搬走了、三个角色各自该干什么、状态流转的控制权放哪、完整的 Java 代码怎么写,以及我在真实项目里踩过的并发和持久化的坑。

1. 从一段被 if-else 撑爆的订单代码说起

1.1 坏味道现场:状态判断散落在每个方法里

先看一段典型的"状态没抽象"的代码,这是我们重构前的真实样子(做了脱敏和简化):

public class OrderService { public void pay(Order order) { if (order.getStatus() == Order.STATUS_PENDING_PAYMENT) { order.setStatus(Order.STATUS_PAID); // 扣库存、记流水 } else if (order.getStatus() == Order.STATUS_CANCELLED) { throw new BizException("订单已取消,不能支付"); } else if (order.getStatus() == Order.STATUS_PAID) { throw new BizException("订单已支付,请勿重复支付"); } else { throw new BizException("当前状态不支持支付"); } } public void ship(Order order) { if (order.getStatus() == Order.STATUS_PAID) { order.setStatus(Order.STATUS_SHIPPED); } else if (order.getStatus() == Order.STATUS_PENDING_PAYMENT) { throw new BizException("未支付不能发货"); } else if (order.getStatus() == Order.STATUS_SHIPPED) { throw new BizException("已发货,请勿重复操作"); } else { throw new BizException("当前状态不支持发货"); } } // cancel、confirm、refund... 每个方法都是同样的结构 }

这段代码有几个很要命的问题,而且它们会随着业务演进不断放大:

  • 同一份状态知识被复制了 N 遍。"哪些状态能支付"这个规则,在pay里写了一遍,在cancel里可能又写了一遍。哪天产品说"待发货状态下已申请退款的订单不能取消",你得把所有方法翻一遍,靠人肉保证不漏。
  • 状态流转的合法性判断和行为执行混在一起。校验和业务逻辑搅在同一个 if 分支里,测试的时候很难单独验证"流转规则"这一层。
  • 新增状态是场灾难。加一个"部分发货",意味着上面每一个方法都要多一个else if,还要重新梳理所有组合。

1.2 状态模式把"判断"搬到了哪里

状态模式的核心动作只有一个:把"状态"从一个 int 常量升级成一个对象,把"在不同状态下对同一个请求的不同响应"从调用方搬到状态对象自己身上。

重构之后,调用方变成了这样:

order.pay(); order.ship(); order.confirm();

没有 if,没有 switch。那么"未支付不能发货"这个规则去哪了?它变成了PendingPaymentState类里的一句话——因为PendingPaymentState根本没有覆写ship方法,父类的默认实现直接抛异常。规则从"对当前状态做判断"变成了"由当前状态自己表态",这就是状态模式的全部魔法。

用一个生活化的类比:以前的写法像是门口保安拿着张表,逐个核对"你是几号状态,这个时间点能不能进";状态模式则是把门禁权限写进了每个人的工牌里,刷卡的时候由工牌自己决定开不开门。保安只负责把卡递给读卡器,不负责判断。

这个转变带来的最大收益是开闭原则的落地:新增一种状态,是新增一个类,而不是修改已有类的代码。对于订单、审批流、工单、设备这类状态会长期演进的业务,这点差异在半年后就是"改两个文件"和"改二十个地方"的区别。

2. 状态模式的三个角色,以及各自不该越的界

2.1 Context:只做转发和存档,不做任何判断

Context(上下文)是外部世界的唯一入口。它持有一个 State 引用,把所有请求原样转给当前状态对象,自己绝不判断"这个状态下能不能干这个"。

很多人写状态模式时,最容易犯的错就是在 Context 里加判断:

// 错误示范:Context 又变成了 if-else 集散地 public void pay() { if (state.getStatus() != OrderStatus.PENDING_PAYMENT) { throw new BizException("不能支付"); } state.pay(this); }

这样做等于把刚搬走的规则又搬回来了一半,状态类的覆写就形同虚设。Context 该干的三件事是:

  • 持有并暴露当前状态(对外通常只暴露一个 status 枚举,而不是 State 对象本身);
  • 负责切换状态,也就是提供setState这类方法,并在切换时做统一记录(日志、埋点、状态变更历史);
  • 转发请求,一个方法一行,纯粹的委托。

2.2 State 抽象:接口还是抽象类,这里有个真实的取舍

状态抽象有两种主流写法,选错了会很难受。

写法一:接口。所有行为都声明在接口里,每个状态类必须实现全部方法。

public interface OrderState { void pay(OrderContext ctx); void ship(OrderContext ctx); void confirm(OrderContext ctx); void cancel(OrderContext ctx); }

它的痛点是:CompletedState这种终态也得把四个方法全写上,方法体里全是throw new IllegalStateException("已完成订单不支持该操作"),五个终态加起来能写几十行重复代码。更麻烦的是,接口一旦新增一个refund()方法,所有状态类都会编译报错,哪怕大部分状态根本不支持退款。

写法二:抽象类 + 默认拒绝。这是我更推荐的写法。

public abstract class OrderState { protected final OrderStatus status; protected OrderState(OrderStatus status) { this.status = status; } public OrderStatus getStatus() { return status; } // 默认语义:当前状态不支持该操作 public void pay(OrderContext ctx) { reject("支付"); } public void ship(OrderContext ctx) { reject("发货"); } public void confirm(OrderContext ctx) { reject("确认收货"); } public void cancel(OrderContext ctx) { reject("取消"); } protected void reject(String action) { throw new IllegalStateException( "订单当前状态[" + status.getDesc() + "],不允许执行[" + action + "]"); } }

这样每个具体状态只写自己支持的动作,比如ShippedState里只有confirm一个方法,其余全部继承默认的拒绝逻辑。新增行为时也不会引发全量编译报错,只需要在真正支持该行为的状态里覆写即可。

提示:默认拒绝有两种实现风格。抛异常适合"非法操作必须被拦截并告警"的场景;返回 boolean 适合"前端按钮置灰、后端静默忽略"的场景。不要在一个项目里两种混用,调用方会疯掉。

2.3 ConcreteState:状态知识唯一的归属地

具体状态类是全部业务规则的落点。它该放什么、不该放什么,值得单独说清楚。

该放的:这个状态下允许什么操作、操作后流转到哪个状态、这个状态下特有的业务校验(比如"待发货状态取消订单时,如果已经出库则需要走拦截流程")。

不该放的:跨状态的通用业务逻辑。比如"扣减库存"这个方法,如果待支付取消和待发货取消都要用,就不要在两个状态类里各写一遍,应该抽到一个独立的领域服务里,状态类调用它。状态类是指挥官,不是苦力。

@Override public void cancel(OrderContext ctx) { if (ctx.isOutboundDone()) { // 已出库,走物流拦截分支 inventoryService.intercept(ctx.getOrderNo()); } ctx.setState(CancelledState.INSTANCE); }

2.4 一张表看清三个角色的职责边界

角色持有数据是否包含业务判断是否决定下一个状态对外可见性
Context订单号、金额、状态引用、变更历史否通常不决定,由状态调用 setState完全可见
State(抽象)状态枚举标识否,只提供默认拒绝否不可见
ConcreteState自身状态标识,必要时带临时数据是,核心规则所在地是不可见
调用方无否否只调 Context 的公开方法

这张表是我在做代码评审时的 Checklist。只要发现 Context 里出现if (status == ...),或者状态类里出现了大段和本状态无关的通用逻辑,基本就可以判定这次抽象没做到位。

3. 状态流转的控制权,到底该放在谁手里

这是状态模式实践中分歧最大的一点,我见过三种做法,各有适用面。

3.1 状态内部自流转:最贴合面向对象直觉

也就是上面代码里的写法——PendingPaymentState.pay()里直接调用ctx.setState(PaidState.INSTANCE)。下一个状态是谁,由当前状态自己决定。

优点是内聚性最强,看一个状态类的代码就知道它的全部门禁规则,不用跳来跳去。缺点是状态类和具体状态类之间产生了直接引用,10 个状态如果两两都可能转移,就形成了一张耦合网。状态数量在 8 个以内,这种方式完全够用;超过 15 个,代码会开始变得难以追踪。

3.2 Context 集中裁决:流转规则一目了然

另一种做法是状态类只表达"我允许这个操作",至于跳到哪个状态,由 Context 查一张转移表决定:

public class OrderContext { private static final Map<String, OrderState> TRANSITIONS = new HashMap<>(); static { TRANSITIONS.put(key(OrderStatus.PENDING_PAYMENT, "pay"), PaidState.INSTANCE); TRANSITIONS.put(key(OrderStatus.PENDING_PAYMENT, "cancel"), CancelledState.INSTANCE); TRANSITIONS.put(key(OrderStatus.PAID, "ship"), ShippedState.INSTANCE); TRANSITIONS.put(key(OrderStatus.PAID, "cancel"), CancelledState.INSTANCE); TRANSITIONS.put(key(OrderStatus.SHIPPED, "confirm"), CompletedState.INSTANCE); } private static String key(OrderStatus status, String action) { return status.name() + ":" + action; } }

这种方式的好处是"所有合法流转"集中在一处,做流程评审、画状态转移图、甚至把这张表配置化下发,都非常方便。代价是状态类里那种"我读到哪儿去"的直观感没了。

3.3 表驱动状态机:状态多到一定程度就该换工具了

当状态超过 20 个、流转规则需要动态配置、还要支持超时自动流转时,硬写状态模式就不划算了。这时候可以考虑引入状态机框架(Spring Statemachine 是 Java 圈比较常见的方案),或者干脆自己写一个表驱动的轻量状态机。

判断依据其实很简单:如果团队里非开发岗(产品、运营)也需要参与流转规则的维护,那就该把它做成配置表,而不是写死在类里。

3.4 三种控制方式的选择对照

方式状态数量级规则集中度新增状态成本适合场景
状态内部自流转3~8分散在各状态类新增一个类 + 改相邻状态订单、工单等中小型流程
Context 集中裁决8~20集中在转移表改表 + 新增一个类规则需要评审和画图的流程
表驱动状态机20+完全外置为配置改配置,可能不动代码审批流、工作流引擎

4. 完整代码示例:订单从待支付到已完成

前面是拆解,这里给一份可以直接跑起来的完整实现。我用 Java 写,选它的原因是状态模式在 Java 里最典型,换成 C++ 只需要把INSTANCE换成静态成员并在.cpp里定义,思路完全一样。

4.1 状态枚举与抽象基类

public enum OrderStatus { PENDING_PAYMENT("待支付"), PAID("待发货"), SHIPPED("已发货"), COMPLETED("已完成"), CANCELLED("已取消"); private final String desc; OrderStatus(String desc) { this.desc = desc; } public String getDesc() { return desc; } }

抽象基类沿用前面 2.2 的写法,这里补一个细节:status字段用protected final,构造时注入,保证状态对象一旦创建,它代表的状态就不会被篡改。

4.2 五个具体状态的实现

public final class PendingPaymentState extends OrderState { public static final PendingPaymentState INSTANCE = new PendingPaymentState(); private PendingPaymentState() { super(OrderStatus.PENDING_PAYMENT); } @Override public void pay(OrderContext ctx) { // 实际项目里:校验金额、冻结库存、生成支付流水 ctx.setState(PaidState.INSTANCE); } @Override public void cancel(OrderContext ctx) { ctx.setState(CancelledState.INSTANCE); } }
public final class PaidState extends OrderState { public static final PaidState INSTANCE = new PaidState(); private PaidState() { super(OrderStatus.PAID); } @Override public void ship(OrderContext ctx) { // 实际项目里:生成运单、推送物流、扣减真实库存 ctx.setState(ShippedState.INSTANCE); } @Override public void cancel(OrderContext ctx) { // 已付款未发货,需要走退款 refundService.refund(ctx.getOrderNo()); ctx.setState(CancelledState.INSTANCE); } }
public final class ShippedState extends OrderState { public static final ShippedState INSTANCE = new ShippedState(); private ShippedState() { super(OrderStatus.SHIPPED); } @Override public void confirm(OrderContext ctx) { ctx.setState(CompletedState.INSTANCE); } }

终态直接留空即可,所有动作都会命中父类的reject:

public final class CompletedState extends OrderState { public static final CompletedState INSTANCE = new CompletedState(); private CompletedState() { super(OrderStatus.COMPLETED); } } public final class CancelledState extends OrderState { public static final CancelledState INSTANCE = new CancelledState(); private CancelledState() { super(OrderStatus.CANCELLED); } }

注意这里我把构造方法都设成了private,并把实例暴露为public static final INSTANCE。理由是这些状态对象不携带任何实例数据,全局一份就够,做成单例能省掉大量无意义的对象分配。这个选择不是可选项,后面 5.1 会讲清楚什么时候它反而是错的。

4.3 Context 与调用方

public class OrderContext { private final String orderNo; private final List<String> history = new ArrayList<>(); private OrderState state; public OrderContext(String orderNo) { this.orderNo = orderNo; this.state = PendingPaymentState.INSTANCE; this.history.add(state.getStatus().getDesc()); } void setState(OrderState next) { OrderStatus from = this.state.getStatus(); OrderStatus to = next.getStatus(); System.out.printf("[%s] 状态流转: %s -> %s%n", orderNo, from.getDesc(), to.getDesc()); this.state = next; this.history.add(to.getDesc()); } public void pay() { state.pay(this); } public void ship() { state.ship(this); } public void confirm() { state.confirm(this); } public void cancel() { state.cancel(this); } public OrderStatus getStatus() { return state.getStatus(); } public String getOrderNo() { return orderNo; } public List<String> getHistory() { return Collections.unmodifiableList(history); } }

setState用的是包级私有(无修饰符),这样只有同包内的状态类能调用它,外部业务代码无法绕过状态规则直接改状态。这是个很小的设计细节,但能堵住"有人图省事直接ctx.setState(...)"这个最常见的破坏点。

4.4 跑一遍,看输出

public class Main { public static void main(String[] args) { OrderContext order = new OrderContext("SO202405210001"); order.pay(); order.ship(); order.confirm(); System.out.println("最终状态: " + order.getStatus().getDesc()); System.out.println("流转轨迹: " + String.join(" -> ", order.getHistory())); OrderContext illegal = new OrderContext("SO202405210002"); try { illegal.ship(); } catch (IllegalStateException e) { System.out.println("拦截非法操作: " + e.getMessage()); } } }

输出结果:

[SO202405210001] 状态流转: 待支付 -> 待发货 [SO202405210001] 状态流转: 待发货 -> 已发货 [SO202405210001] 状态流转: 已发货 -> 已完成 最终状态: 已完成 流转轨迹: 待支付 -> 待发货 -> 已发货 -> 已完成 拦截非法操作: 订单当前状态[待支付],不允许执行[发货]

从这里能看出状态模式一个很实用的副产品:history列表天然就是一条完整的流转链路,出问题时直接把它打到日志里,排查线上事故的效率能提升一大截。我们后来还把它和变更时间戳一起落到数据库,做了个简单的状态流转审计表。

5. 状态对象谁来创建:单例、新建还是交给工厂

这个问题看起来琐碎,实际上是我在 code review 里发现 Bug 最多的地方。

5.1 无状态的状态对象,放心用单例

判断标准只有一条:这个状态类有没有自己的实例字段。如果像ShippedState那样只有一个继承来的status标识,那它就是无状态的,全局共享一个实例完全安全,还省内存。这也是我在 4.2 里全部写成INSTANCE的原因。

5.2 有状态的状态对象,做成单例就会串号

反过来,只要状态类里出现了属于"某一次流转过程"的字段,就绝对不能共享。举个真实例子,我们做过一个带重试的中间状态:

// 危险写法:这个 retry 会被所有订单共享 public class PayingState extends OrderState { public static final PayingState INSTANCE = new PayingState(); private int retry = 0; // 属于单个订单的运行时数据 @Override public void pay(OrderContext ctx) { if (++retry > 3) { ctx.setState(FailedState.INSTANCE); } } }

这段代码在单线程压测下毫无问题,一上生产就会出现"订单 A 的支付重试把订单 B 的次数带到了 3"。因为retry是挂在单例上的,而单例是全应用共享的。正确做法有三种:把这个字段挪到 Context 里、每次调用时new PayingState()、或者干脆用一个Map<订单号, Integer>显式记录。我更倾向于第一种,因为 Context 本来就是承载订单运行时数据的地方。

提示:判断能不能做单例,不要看"这个字段是不是很少变",而要看"这个字段属于上下文还是属于状态本身"。属于上下文的,一律放 Context。

5.3 用枚举实现轻量版状态模式

如果状态只有三四个、行为也很简单,用完整的类体系会显得很重。Java 的枚举可以带抽象方法,天生适合这种场景:

public enum SimpleOrderStatus { PENDING_PAYMENT { @Override public SimpleOrderStatus pay() { return PAID; } @Override public SimpleOrderStatus cancel() { return CANCELLED; } }, PAID { @Override public SimpleOrderStatus ship() { return SHIPPED; } @Override public SimpleOrderStatus cancel() { return CANCELLED; } }, SHIPPED { @Override public SimpleOrderStatus confirm() { return COMPLETED; } }, COMPLETED, CANCELLED; public SimpleOrderStatus pay() { throw new IllegalStateException("当前状态不支持支付"); } public SimpleOrderStatus ship() { throw new IllegalStateException("当前状态不支持发货"); } public SimpleOrderStatus confirm() { throw new IllegalStateException("当前状态不支持确认"); } public SimpleOrderStatus cancel() { throw new IllegalStateException("当前状态不支持取消"); } }

调用方变成status = status.pay();,一行搞定,还自带持久化能力(存字符串就行)。它的天花板在于:状态里没法方便地注入 Spring 的 Bean,做不了复杂的数据库操作。所以我的经验是——纯规则流转用枚举,涉及外部资源操作的用类体系。

6. 状态模式、策略模式、责任链:别再用混了

面试被问"状态模式和策略模式有什么区别",很多人答不上来。我一开始也答不上,后来用一个类比想通了:策略模式是"选一次就一直用",状态模式是"用着用着会自己变"。

维度状态模式策略模式责任链模式表驱动状态机
核心意图对象行为随内部状态改变运行时选择一种算法请求沿链传递直到被处理用数据描述流转规则
状态/分支之间是否互相知晓是,状态自己决定下一个否,策略之间互不感知部分知晓(持有 next)否,全部由表描述
流转是否自动发生是否,由调用方指定是,沿链前进是
典型落地订单、工单、设备状态支付渠道、折扣算法审批、过滤器链复杂审批流
新增一个分支的成本新增类 + 改相邻状态新增类 + 改选择逻辑新增类 + 调整链顺序改配置表

还有一个容易混淆的点:状态模式和有限状态机(FSM)不是一回事。状态模式是用面向对象的方式实现 FSM 的一种手段,FSM 是概念模型,状态模式是实现手法。理解这一层,就不会纠结"到底该用状态模式还是状态机"了——它们是不同层级的词。

顺带说一句课程作业的场景。不少同学做设计模式大作业时,会把状态模式和简单工厂模式硬凑在一起:状态对象的创建交给一个工厂。这本身没错,但如果工厂里又是一长串if-else判断该 new 哪个状态,那就是把刚消灭掉的条件分支又请回来了。要真想结合,用枚举 +values()做映射比if-else干净得多。

7. 优缺点摆在桌面上,以及不该用它的三种情况

7.1 它真正解决的问题

  • 消除庞大的条件分支。原本散落在各个方法里的状态判断,被压缩成每个状态类里的几行覆写。我们那次重构,OrderService从 800 行降到 300 行出头,减少的主要就是重复的 if 分支。
  • 状态知识单点收敛。"哪些状态能取消"这个问题,答案只存在于各个状态类中,不会存在第二份副本。
  • 新增状态符合开闭原则。加"部分退款"状态,只需新增一个类并修改触发它的那个相邻状态,其余代码零改动。
  • 流转过程天然可观测。所有状态变更都经过 Context 的一个setState出口,想加日志、埋点、审计、发消息,都只需要改这一个地方。

7.2 它的成本,也要说清楚

  • 类数量膨胀。5 个状态就是 5 个类,20 个状态就是 20 个类。IDE 里一屏都放不下,新人接手时容易迷路。
  • 状态爆炸难以避免。当状态和行为都很多时,理论上可能出现"状态 × 行为"的组合膨胀,这时候往往需要重新审视状态划分是否合理,而不是继续硬加类。
  • 调试链路变长。以前打断点在一个方法里就能看完,现在要跟着setState一路跳。Context 层的统一日志能很大程度上缓解这个问题。

7.3 三种不该用状态模式的场景

  • 状态只有两三个,且短期不会增长。比如一个只有"启用/禁用"两态的对象,写个if就够了,硬套状态模式属于杀鸡用牛刀。
  • 状态之间完全没有行为差异。如果所有状态对所有操作的响应都一样,那状态模式带来的只是纯粹的类数量负担。
  • 状态流转规则高度动态且必须由运营配置。这种情况直接用配置驱动的状态机,比手写状态类更合适。

8. 踩坑实录:并发、持久化与初始化顺序

下面这三个坑,都是我在生产环境真真切切踩过的,写出来给后来人省点时间。

8.1 并发下的状态覆盖:单例没问题,Context 有问题

前面说了状态对象可以做单例,但这不代表状态模式天然线程安全。问题出在 Context 上——state字段是被多方共享的可变状态。

试想订单 A 在两个线程里同时收到"支付"和"取消"请求(用户手速快 + 网络重试,这在真实场景里完全可能),两个线程都读到了PendingPaymentState,然后一个 set 成PaidState,一个 set 成CancelledState,最终状态取决于谁后写,而支付流水可能已经产生了。这就是典型的检查后执行竞态。

我的处理方案分两层:

  • 业务层用乐观锁兜底。数据库加version字段,UPDATE ... WHERE order_no = ? AND status = ? AND version = ?,状态变更以数据库的原子更新为准,应用层状态只是一个缓存视图。
  • Context 层加同步。如果业务量不大,直接给setState和状态方法加synchronized;如果既要并发又要性能,可以用AtomicReference<OrderState>配合 CAS,只有状态没被别人抢先改过才允许流转。
public boolean compareAndSetState(OrderState expect, OrderState update) { return stateRef.compareAndSet(expect, update); }

8.2 持久化:状态对象该存什么、不该存什么

状态类里那些INSTANCE是内存里的对象,数据库里是存不进去的。我的做法是:数据库只存OrderStatus这个枚举的字符串值,重建对象时按枚举反查状态实例。

public OrderContext restore(String orderNo, OrderStatus status) { OrderContext ctx = new OrderContext(orderNo); ctx.setState(OrderStateRegistry.of(status)); // 枚举 -> 状态实例 return ctx; }

OrderStateRegistry就是一个Map<OrderStatus, OrderState>,初始化时把五个单例塞进去。这一步千万别用switch写反查逻辑,不然状态一多,反查会变成全项目最难维护的一段代码。

这里还有个隐藏陷阱:如果某个状态类带了运行时字段(像前面那个retry),那它根本无法从数据库恢复,因为重启后这个值就丢了。所以我在 5.2 里才强调要把它放到 Context 里,而 Context 的数据本来就是要持久化的。

8.3 循环引用与静态初始化的隐秘顺序问题

如果两个状态互相在对方类里引用单例,比如PendingPaymentState和CancelledState互相import并访问INSTANCE,一般情况下没问题,但一旦涉及复杂的静态初始化块,就可能出现"读到 null"的诡异现象——因为 JVM 是按需初始化类的,A 初始化到一半去访问 B,B 又回头访问还没初始化完成的 A。

避坑办法很朴素:状态类里不要写静态初始化块,所有状态实例的注册统一放到一个独立的注册表类里。这样初始化顺序就完全由你自己控制,问题从根上消失了。

提示:如果你的状态之间有"互相引用"的感觉,先停下来想一想,大概率是状态划分粒度太细了,应该合并。我见过一个项目把订单状态拆到 23 个,其中一半是可以合并的中间态。

8.4 别忘了在切状态时打日志

最后这个算不上坑,但收益极高。所有状态变更都收敛在setState一个方法里,在这里打一行结构化日志,成本几乎为零:

void setState(OrderState next) { OrderStatus from = this.state.getStatus(); OrderStatus to = next.getStatus(); log.info("order_state_transition orderNo={} from={} to={} traceId={}", orderNo, from.name(), to.name(), TraceContext.getTraceId()); this.state = next; this.history.add(to.getDesc()); }

线上遇到"状态怎么不对"的投诉时,这行日志能直接告诉你状态在哪一步被谁改的。我们上线这行日志之后,状态相关的排查时间从平均半小时压到了几分钟。

我在实际项目里用状态模式的体会是,它真正的价值不在于省了多少行代码,而在于它把"状态流转"这件事变成了一个可以单独做单元测试的模块。以前要验证"已发货订单不能取消",得构造一个订单、走完支付和发货流程、再调取消,断言抛异常;现在直接new OrderContext(...)、setState(ShippedState.INSTANCE)、调cancel(),五行测试代码搞定。如果你团队里也有那种改一处状态规则要跑半小时回归测试的模块,状态模式值得试一次。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:27:52

Win7蓝屏排查实战:配置转储文件并用WinDbg定位驱动故障

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:27:51

STM32开发必备:国内高效资源平台与参考方案全指南

1. 为什么“找参考方案”比“从零写代码”更值得花时间STM32 这颗芯片在国内嵌入式圈子的地位&#xff0c;用一句话概括就是&#xff1a;你绕不开它。从高校实验室的毕业设计&#xff0c;到工业现场的电机控制板&#xff0c;再到消费电子里的智能台灯、鱼缸控制器&#xff0c;S…

作者头像 李华
网站建设 2026/9/30 1:27:49

BMS上车前必须经历哪些测试?从功能验证到失效安全的完整流程

一块BMS板子从画好PCB、打好样、焊完器件&#xff0c;到真正装进电池包上车&#xff0c;中间隔着的不只是几次“测试通过”的邮件&#xff0c;而是一整套能把设计逼出原形的验证流程。我入行做电池管理系统那会儿&#xff0c;最天真的想法就是“板子能跑、采样准、通信通”就能…

作者头像 李华
网站建设 2026/9/30 1:27:22

NAT本质是户籍管理而非地址翻译:会话表驱动的排障方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:26:56

AIoT范式迁移:从设备联网到边缘智能协同

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:26:38

JLink、STLink、DAPLink三大嵌入式调试器对比与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华