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

相关新闻