复杂流程最容易变乱的地方,不是代码一开始写错了,而是状态被一点点藏起来了。
一开始,流程可能很简单。一个请求进来,做解析,做校验,生成结果。几个 if 就够了。
后来业务开始变化:有些情况要重试,有些情况要等用户确认,有些情况要从上一轮继续,有些情况不能继续但也不能直接失败。于是代码里慢慢出现了很多字段:
text
resolved
pending
retryCount
currentIndex
snapshot
answerJson
ambiguous
hasMore
每个字段单独看都合理。但问题是,真正的业务状态不在任何一个字段里,而在这些字段的组合里。
维护者想知道"系统现在到底在哪里",必须在脑子里同时拼出一组判断:
text
snapshot != null
answerJson != null
resolved == false
ambiguous == true
currentIndex < total
retryCount < 1
这时,流程已经不是靠代码表达了,而是靠人脑临时还原。
这就是复杂流程变乱的起点。
架构的目标,是减少脑内状态
很多人把架构理解成"增加层次"。但真正有价值的架构,不是层越多越好,而是让系统更容易被理解、验证和修改。
如果一个改动要求维护者同时记住:
text
当前字段
历史快照
前端回答
当前下标
重试次数
是否 pending
是否 resolved
是否 ambiguous
那系统复杂度就已经压在人身上了。
状态机的意义,是把这些负担从人脑里拿出来,放回系统表达里。
好的状态机不一定复杂。它可能只是几个状态名,一张转移表,几个 helper 方法。
但它能让团队快速回答三个问题:
text
我现在在哪里?
我为什么来到这里?
我接下来能去哪里?
能回答这三个问题,流程就不再只是代码的堆叠,而开始有了架构。
所以,状态机不是架构炫技。
它是一种克制的整理方式:当流程已经复杂到需要人脑猜测时,把状态命名出来,把转移写清楚,把隐含秩序显性化。
真正的架构不是增加层,而是减少维护者必须在脑子里同时保存的隐含状态。
一、流程变乱,是因为状态被藏起来了
很多系统并不是没有状态,而是状态没有名字。
比如一个流程里出现了这些判断:
text
如果有历史快照,就恢复上下文
如果有用户回答,就合并答案
如果还有未处理项,就继续推进
如果发现歧义,就返回前端
如果已经解决,就跳过当前链路
这些判断背后其实已经有状态:
text
正在恢复
正在处理
等待用户
继续推进
已经完成
但如果代码里没有这些名字,维护者看到的就只是零散字段和分支。
这会带来一个很大的问题:每一次维护,都要重新推理一遍流程。
改一个 bug 时,要先弄清楚:
text
这个 resolved 是本轮解决,还是上一轮解决?
这个 pending 是等用户,还是等自动修复?
这个 index 指当前项,还是下一项?
这个 snapshot 是输入协议,还是内部机器态?
当这些问题不能被代码直接回答时,系统就开始依赖"熟人知识"。
老同学知道哪里不能动,新同学只能小心试。架构的负担没有消失,只是转移到了人的脑子里。
二、状态机,就是让状态显性化
状态机的本质很简单:
text
当前在哪里 + 遇到什么事 + 接下来去哪
也可以写成:
text
state + event -> next state
它不是一个框架,也不是一种复杂设计模式。它首先是一种表达方式。
例如,一个复杂流程可以从零散字段,翻译成这些明确状态:
text
PARSING 正在解析
PROCESSING 正在处理
WAITING_USER 等待用户输入
RESUMING 正在从用户回答恢复
RETRYING 正在重试
RESOLVED 已完成
BLOCKED 已阻断
一旦状态有了名字,很多问题就变简单了。
之前维护者要问:
text
为什么 resolved=false,但又没有继续执行?
为什么有 snapshot,还重新解析了?
为什么用户回答了,还是继续澄清?
现在可以问:
text
当前是不是 WAITING_USER?
收到 ANSWER_RECEIVED 后,是否应该进入 RESUMING?
RESUMING 完成后,应该回到 PROCESSING,还是直接 RESOLVED?
这就是状态机最大的价值:它把隐含流程变成可讨论的业务语言。
状态机不是让代码多一层,而是让维护者少猜一层。
三、能暂停、能恢复、要记住位置,就是状态机
不是所有流程都需要状态机。
一个简单函数,输入参数,返回结果,中间没有暂停、没有恢复、没有跨请求上下文,那不需要状态机。硬加状态,只会增加复杂度。
但如果一个流程满足三个条件,就应该用状态机视角去看它。
第一,它会暂停。比如系统处理到一半,发现信息不够,需要等用户确认;或者要等人工审核;或者要等外部系统回调。
第二,它会恢复。用户回答后,系统不是重新走一个全新流程,而是要接上之前停住的地方继续。
第三,它要记住位置。恢复时,系统必须知道之前处理到哪里了,当前项是什么,哪些已经解决,哪些还没解决。
这三个条件合起来,就是典型状态机:
text
能暂停
能恢复
要记住上次停在哪里
这类流程如果没有显式状态,最后一定会用各种字段拼出来:
text
snapshot 表示暂停现场
currentIndex 表示处理位置
pending 表示等待输入
answerJson 表示恢复事件
resolved 表示是否完成
字段越来越多,状态越来越隐蔽。
所以判断一个流程需不需要状态机,不要先问"要不要引入状态机框架",而要问:
text
它是不是已经在用字段模拟状态机?
如果答案是肯定的,架构问题就不在于是否增加抽象,而在于是否应该把这些隐形状态命名出来。
四、不引框架,先命名状态和转移
Ponytail 的思路不是上来重写,也不是上来引框架。
它会先问:第一个能解决问题的台阶是什么?
面对混乱流程,第一个台阶通常不是新框架,而是命名。
先命名状态:
text
PARSING
PROCESSING_ITEM
WAITING_USER_INPUT
RESUMING_FROM_ANSWER
RESOLVED
BLOCKED
再命名事件:
text
PARSE_SUCCESS
ITEM_OK
AMBIGUITY_FOUND
ANSWER_RECEIVED
NO_MORE_ITEMS
UNRECOVERABLE_ERROR
然后写清楚转移:
text
PARSING + PARSE_SUCCESS -> PROCESSING_ITEM
PROCESSING_ITEM + ITEM_OK -> PROCESSING_ITEM
PROCESSING_ITEM + AMBIGUITY_FOUND -> WAITING_USER_INPUT
WAITING_USER_INPUT + ANSWER_RECEIVED -> RESUMING_FROM_ANSWER
RESUMING_FROM_ANSWER + ANSWER_MERGED -> PROCESSING_ITEM
PROCESSING_ITEM + NO_MORE_ITEMS -> RESOLVED
PROCESSING_ITEM + UNRECOVERABLE_ERROR -> BLOCKED
做到这里,很多时候已经够了。
代码不一定马上拆成很多类,也不一定需要一个状态机引擎。只要状态、事件、转移被显式表达,维护者就能知道系统此刻在哪里。
下一步才是收口判断。
把散落在各处的字段组合:
java
snapshot != null && answerJson != null && !resolved
收口成一个业务方法:
java
isResumingFromAnswer()
把字符串判断:
java
resultJson.contains("\"ambiguous\":true")
收口成:
java
hasPendingClarification()
把下标判断:
java
currentIndex < items.size()
收口成:
java
hasMoreItems()
这就是最小改造。
不改变业务结果,不大拆结构,不急着抽象,只是让原本藏在脑子里的状态进入代码。