状态一多,接口为什么就开始变复杂了
这篇算是把前面那几篇任务调度、异常降级、日志权限的收拢篇把。
很多时候接口变复杂,表面上看像是参数变多了、判断变多了、页面要求变多了。真把业务往后做一段时间,会发现很多复杂度其实都长在状态上。状态一开始看起来只是几个字段,后来会慢慢变成整个系统动作边界的承载点。到这个阶段,接口到底能不能继续收得住,很大程度取决于状态有没有先被定义清楚。
1. 一开始看状态字段,很容易觉得它只是个辅助信息
刚开始做接口时,状态字段通常不会显得特别“重”。
最直观的理解大概都是这样:
- 0 是待处理
- 1 是处理中
- 2 是成功
- 3 是失败
看起来很普通。
很多时候人也会本能地把它当成:
- 页面要显示的文案来源
- 列表筛选条件
- 一个顺手补上的字段
在功能少、链路短的时候,这种理解勉强也能用。
因为这时候动作不多,状态也不多,接口里写几句判断就过去了。
但项目只要稍微往后推一点,状态就会开始膨胀。
比如 vchoo 这种链路里,很快就会碰到:
- 创作任务状态
- 故事生成状态
- 分镜拆分状态
- 生图子任务状态
- 回调回写状态
- 取消状态
- 重试状态
- 部分成功 / 部分失败状态
到了这个时候,状态就不再只是页面显示用的字段了。
它开始承担一件更核心的事:
系统当前允许做什么,不允许做什么。
这也是我后来对状态理解变化最大的地方。
2. 接口后来会变复杂,很多时候是因为动作都带上了前置状态
这是我后来一个很强烈的感受。
刚开始做接口时,很容易把接口理解成一个动作入口:
- 创建
- 修改
- 删除
- 重试
- 取消
动作看起来都很直接。
但一旦业务链长起来,这些动作都会开始带前置条件。
比如“取消任务”这件事,表面看就是点一下按钮。
真进系统以后,你就得先判断:
- 当前任务是不是还没执行
- 当前任务是不是已经在执行中
- 当前任务是不是已经成功
- 当前任务是不是已经失败
- 当前任务是不是已经取消过了
动作没有变,但动作能不能执行,开始被状态限制住了。
一旦这种限制变多,接口复杂度就会跟着上来。
因为 Controller、Service 甚至回调处理里都会不断出现类似问题:
- 这个状态下允不允许改
- 那个状态下还能不能重试
- 已取消的任务回调回来后要不要更新结果
- 部分成功时整体状态怎么算
所以接口越往后越重,通常不是代码突然写差了。更多时候,是业务动作开始被状态卡住了:
业务动作开始需要被状态约束。
3. 状态一多,先变重的往往是后端判断链
前端当然会感知到状态复杂。
比如:
- 按钮什么时候禁用
- 页面显示什么提示词
- 当前卡片展示哪个标签
但真正先被状态拖复杂的,其实通常是后端。
前端更多是在展示,后端则要负责拍板。
比如一个看起来普通的接口,后端可能要先走完这样一串判断:
- 主任务当前在什么状态
- 子任务当前在什么状态
- 上一步有没有完成
- 这次动作会不会和当前状态冲突
- 这个状态是不是已经被别的异步动作改过了
- 当前这个写入会不会覆盖掉更晚的真实结果
只要这类判断一多,接口很快就不再是“收参数、调服务、返回结果”这么平了。
它会慢慢长出很多分支,而这些分支如果没有一个统一的状态理解,就会显得特别乱。
4. 状态不是展示字段,它本质上是在描述业务约束
这个点是我后来比较认同的一层理解。
因为只把状态看成展示字段,后面很多设计都会歪。
比如:
- 页面想显示“处理中”,那就加一个状态
- 页面想显示“已完成”,那就再补一个状态
- 页面想区分“部分成功”,那就继续加一个状态
这样做的问题是,状态会越来越像页面文案表,而不是业务规则表。
可系统真正在意的,根本不只是“叫什么名字”,而是:
- 这个状态下还允不允许做某个动作
- 状态能不能从 A 直接跳到 C
- 这个变化是不是必须经过中间步骤
- 某个状态失败后,下一步到底是回退、重试还是终止
所以我后来会更愿意把状态理解成:
状态字段是在描述业务对象当前所处的约束区间。
这个对象现在能做什么、不能做什么,都得靠它来收。
一旦这样想,状态设计和接口设计就会自动绑在一起。
5. 状态一多以后,最难收的其实是边界
很多人一谈状态复杂,第一反应是状态太多不好记。
我现在回头看,记不住状态名字其实还不是最麻烦的。
真正难的是:
哪些状态属于主任务,哪些状态属于子任务,哪些状态只是某一层执行过程里的临时状态。
如果这一层不拆清楚,后面状态会相互污染。
比如在 vchoo 这种链路里:
- 创作任务有整体状态
- 分镜有自己的状态
- 生图任务又有自己的状态
如果你把这些状态全揉在一处,后面很容易出现:
- 主任务状态和子任务状态打架
- 前端查整体进度时拿到的是局部状态
- 某个分镜失败以后,整次任务立刻被打成失败
- 某次局部重试把主状态覆盖乱了
所以我后来会越来越强调一件事:
状态要先分层,再分值。
先确定是谁的状态,再去定义它有哪些值。
这个顺序很重要。
6. 状态流转一旦没有规则,接口就会慢慢变成一堆 if else
这点其实在很多系统里都会发生。
一开始功能少的时候,写几个判断并不明显:
if (task.Status == Pending) { ... }
if (task.Status == Running) { ... }这种写法前期完全能接受。
问题是状态一多、动作一多、异步一多,这种判断会开始成倍增长。
后面你会在很多地方看到类似逻辑:
- Controller 里判断一次
- Service 里再判断一次
- 回调里再补一次
- 重试时又判断一轮
- 取消时再加一套分支
最后代码里到处都是状态相关的 if else。
复杂度真正往上窜,倒不是因为多写了几个 if else。问题在于:
状态流转规则没有被收住。
每个地方都在自己理解“当前状态还能不能做这件事”,系统就会越来越难维护。
所以我后来会更认同这样的方向:
- 状态流转要有明确规则
- 规则尽量往业务对象或领域层收
- Controller 和外围层尽量别各写一套自己的状态判断
不然接口最终会复杂到很难改。
7. 回调、重试、取消这些动作一进来,状态设计就从“静态标记”变成“动态协商”
这也是我在任务系统里感受特别明显的一点。
如果系统没有异步任务,很多状态变化是顺序单线程的,问题还没那么明显。
但一旦有了:
- 回调
- 重试
- 取消
- 轮询查询
状态就不再只是单个地方自己改一下那么简单了。
因为这几类动作可能发生在不同时间点,而且都想去写同一个对象的状态。
比如:
- 用户刚点了取消
- 后端把状态标成已取消
- 结果底层回调晚了一点回来
- 回调又想把状态改成成功
这时候大家争的已经不是字段名了,真正要定的是:
谁有资格在什么时候覆盖哪个状态。
所以从这个阶段开始,状态设计已经和并发、异步、最终一致性这些问题直接连上了。
也正因为这样,接口复杂度会迅速放大。
因为后端在处理接口时,不只是做业务动作,还得处理状态写入时机和覆盖顺序。
8. 我后来会把“状态复杂”拆成三层来看,这样脑子会清楚很多
这也是我后来给自己留下的一个比较有用的看法。
状态一多以后,如果全堆在一起看,很容易乱。
我现在会更倾向于拆成三层:
第一层:业务阶段状态
比如:
- 草稿
- 待执行
- 执行中
- 已完成
- 已失败
- 已取消
这一层最贴近业务动作。
第二层:执行过程状态
比如:
- 已入队
- 已下发
- 回调处理中
- 已落库
这一层更像执行链内部状态。
第三层:展示状态
比如前端最终显示的:
- 处理中
- 部分完成
- 可重试
- 已终止
这三层如果不区分,后面很容易出现一个字段既想表达业务意义,又想表达执行细节,还想直接给前端拿去显示。
结果就是谁看都别扭。
这个拆法对我帮助很大。
至少它让我知道:
- 不是所有状态都该塞进一个字段
- 不是所有状态都该直接暴露给前端
- 不是所有状态都该用来做业务判断
9. 回头看,很多接口之所以越写越重,是状态一直没被真正建模
我现在对这件事的一个判断是:
很多接口之所以越写越重,不一定是因为写代码的人水平不够,也不一定只是分层没分好。
更多时候,问题根源在于:
状态还只是字段,还没有真正变成业务模型的一部分。
一旦状态还没建模,后面所有动作都只能在外围层补判断。
- 页面补一层
- 接口补一层
- Service 补一层
- 回调再补一层
每一层都在试图理解“现在应该怎么办”,系统自然越来越复杂。
所以我后来会越来越觉得,状态设计其实很像架构设计的一部分。
它看着像字段,实际上在决定几件更底层的事:
- 业务对象怎么变化
- 接口怎么控制动作边界
- 失败以后系统怎么继续往下走
10. 结论
- 状态一开始看着像辅助字段,后面往往会变成业务约束的承载点
- 接口会变复杂,常常是因为动作开始受状态约束
- 状态要先分层,再定义取值,不然主任务和子任务很容易互相污染
- 状态流转规则如果没收住,接口代码很快就会长成一片 if else
- 回调、取消、重试一进来,状态问题就会直接变成一致性问题
这篇先记到这里。
