状态机、TDD与上下文管理:把需求文档从作文变成图纸
我做了这么多年技术踩过最深的坑就是需求文档写不清楚。尤其是那种牵扯到多个状态的业务比如订单流转、审批流、设备控制你用再多的自然语言去描述开发同学照样能给你理解歪掉最后做出来的东西跟你脑子里想的完全不是一回事。今天这篇实战技巧我就把状态机、TDD、上下文管理这三板斧聊透它们不是花架子是真的能把需求文档从“作文”变成“图纸”的硬核工具。先说状态机。很多需求文档里的逻辑是这么写的“如果用户已经支付了那就可以发货如果还没支付那就要先支付才能发货如果支付失败了……”。这套描述看着没什么问题但是当状态多了以后各种“如果”会像毛线团一样缠在一起压根理不清。而状态机干的事情就是把“状态”和“事件”抽出来让每一个“如果”都落在明确的节点上。说白了一篇合格的需求文档应该能做到“在任何给定的状态下面对任何可能事件系统行为都是确定的”。做不到这一点开发同学就只能靠猜猜错就返工返工就延期延期大家就一起背锅。TDD这个东西呢很多人觉得只是写代码的时候用的跟需求文档没关系。但我的理解恰好相反TDD最核心的思想不是“先写测试”而是“先定义验收标准”。你写需求的时候如果能把“什么样的行为算通过”“什么样的输入会造成什么样的输出”写得一清二楚那这个需求文档的可执行性会直接上一个台阶。因为代码里跑的是断言需求里跑的是验收标准这两者本质上是一回事。至于上下文管理这个东西更隐蔽但更致命。很多需求文档写着写着就失控了老板说加一个字段你就加运营说改一个规则你就改产品评审会开完文档里已经堆了几十个名词每个名词在不同章节里的含义还不一样。上下文管理就是帮你把这些概念的边界、生命周期、可见范围定清楚让文档里的每一个词都有且只有一个意思。接下来我结合实操场景把这三种技术怎么落到需求文档里一个个说清楚。1. 整体思路拆解为什么状态机、TDD、上下文管理能治需求文档的“病”我把需求文档写得不好的典型症状归三类状态爆炸、验收缺失、概念混乱。这三类症状分别对应的就是状态机、TDD、上下文管理这三个药方。它们不是三个割裂的工具而是一条从“行为建模”到“验收确认”再到“结构治理”的完整链路。1.1 状态机把“怎么做”变成“在什么状态下做什么”需求文档中有大量业务规则其实都是对“某种状态下发生某种事情该如何处理”的描述。用自然语言描述这类规则很容易出现两条规则互相覆盖的情况而状态机能用有限的状态集合把所有可能的路径画出来。我之前接手过一个售后工单系统原文档里描述退货、换货、维修三种流程混在一起写开发做完一期上线后才发现“已退款”的工单还能被操作成“已关闭”线上数据一塌糊涂。后来我们花了一个下午把所有状态梳理出来画了一张状态图问题当场就暴露出十几个。状态机的建模方式特别适合那种“生命周期明确、流转路径可控”的领域电商订单、审批流、任务调度、设备控制、协议解析统统适用。你在需求文档里放上状态图然后配一张状态转移表把每个跳转的前置条件和后置动作写清楚开发同学看到这份文档脑子里基本不需要再做二次翻译。还有一点容易被忽略状态机不只是用来画图的它还能帮你做需求完整性检查。你画完状态图之后可以检查一下“每个状态是否都有入口和出口”“除了终止态之外每个状态是否都有离开路径”“有没有状态从来没被任何事件触发到”。这些问题如果写描述性文档根本无从检查但是用状态机一目了然。1.2 TDD把需求文档改造成“可执行”的验收标准很多人觉得TDD是开发方法论用它指导写需求文档有点“越界”。但实际上TDD的思考方式非常适合用来打磨需求先写一个会失败的测试也就是一条验收标准再写让测试通过的实现也就是具体的业务流程。对应到需求文档里就是先定义好“怎样的输出算正确”再去描述系统应该怎么处理。你如果能做到每一条需求下面挂上3到5条验收标准你的需求文档就不会再是“描述了一个功能”而是“定义了一批可被验证的行为”。TDD式验收标准的写法我推荐用Given-When-Then结构。Given指的是前置条件When是事件触发Then是期望的、可观测的结果。这种结构化表达方式能覆盖正常路径、异常路径、边界值而且不容易漏测。比如“用户申请退款”这条需求至少得拆出几个场景全额退、部分退、金额超限、订单已部分发货、支付渠道已关闭等。用TDD的思路写需求还有一个额外的好处开发做技术方案的时候能直接引用你的验收标准来写单测、写接口测试用例测试开发也能直接拿它来写集成测试脚本。测试用例不是从代码里长出来的而是从需求里长出来的。1.3 上下文管理给文档里的每个名词一个“唯一归宿”写需求文档最怕的就是词汇失控。同一个“用户”有时候指的是C端顾客有时候指的是后台管理员有时候指的是第三方系统对接账号这谁看得懂上下文管理的核心思想是在文档中明确划分概念边界让每个名词在特定的上下文里只有一个含义。我常用的做法是在文档开头设置“术语与上下文”章节定义一个上下文清单。比如“用户”这个词在“用户端下单流程”这个上下文里指的就是C端消费者在“后台审核流程”这个上下文里指的是运营人员。然后在后续描述中尽量带上限定词不偷懒。再比如“订单状态”和“物流状态”这两个概念经常有人混着用订单状态十几种物流状态好几种混在一起必然出Bug。用上下文清单把它们分清楚后续的所有流程描述都能少很多歧义。上下文管理还有一层意思叫做“控制的纵深”。一份好的需求文档不该是平铺直叙的而应该像代码一样有层级有作用域。顶层的流程文档只讲主干和关键分支细节规则下沉到子章节跟我下面要讲的文档组织结构是密不可分的。2. 核心细节解析与实操要点把这三个工具真正用起来理论讲完了该讲讲实践了。这一章的内容全部来自我实际写文档的复盘没有一点空想。2.1 状态机建模的五个关键要素一个完整的状态机建模需求文档里至少要包含五个要素状态集合系统中有哪些稳定的、可区分的状态注意状态是“稳定”的不是瞬间的。比如“订单创建中”就不是一个合适的中间状态它应该是一个瞬时过程你只需要定义“创建成功”和“创建失败”。事件集合哪些动作会触发状态迁移事件可以是用户操作、系统回调、定时器触发、第三方通知等。转移规则从A状态到B状态需要什么事件以及什么前置条件这是需求文档中最核心的部分必须精确到“可判定”。动作状态迁移时系统需要执行哪些副作用操作比如发短信、调用支付接口、写日志。初始态和终止态一个状态机至少要有一个初始态可以有多个终止态也可以是死循环状态机。这两个节点是状态完备性检查的锚点。我在写需求文档的时候经常用一张表格把以上五要素全装进去表格的每一行是一次状态迁移需要列清楚当前状态、触发事件、前置条件、动作、目标状态。这样做的另一个好处是这套表可以直接发给后端开发让他照着一张表把State Pattern或者状态机引擎的配置写出来开发效率翻一倍都不止。2.2 需求文档中的TDDGiven-When-Then实操写法在需求文档中落实TDD并不需要真去写代码但你要学会像写代码一样写“断言”。我拿一个最简单的登录功能举例Given一个已注册但未激活的用户When该用户调用登录接口Then系统返回“账号未激活”且不允许进入主界面这就是一条可执行的需求。如果你在文档里写的是“未激活账号不允许登录”开发怎么实现他可能只在登录接口判断服务端有没有激活标记但如果在App端缓存了上次的登录态呢用户直接绕过登录界面进入主界面呢这就是需求文档太笼统造成的隐患。用Given-When-Then写的时候要特别注意把“不可见条件”和“可观测行为”分开。前置条件是系统内部数据状态期望结果必须是外在可感知的信号——接口返回值、页面跳转、数据库记录、第三方请求报文等。要求能观测是为了让测试能断言也让验收有据可依。我还建议在每个重要模块的需求后面附带一个“场景覆盖矩阵”横轴是业务场景比如正常支付、余额不足、重复支付、支付超时、风控拦截纵轴是每个场景是否写了验收标准、是否覆盖了技术测试用例。这个矩阵挂在文档末尾既能防止自己漏写也能让测试和开发一眼看到覆盖情况。2.3 上下文管理的三层落地方法上下文管理听着抽象落地其实就三件事分层、限定词、生命周期表。分层是指文档在组织结构上必须区分核心流程上下文和支撑流程上下文。比如“购物车”和“结算页”是两个对立的上下文虽然购物车可以从结算页进入但是购物车内看到的价格展示规则和结算页的最终金额核算规则不应该写在同一个章节里。我以前见过有需求文档把这两个场景写在一坨最后“购物车享受满减”和“结算页不享受满减”的bug测试测了三轮都没发现因为测试照着文档的同一段描述去测的自然测不出矛盾。限定词的意思很简单全文书写不要使用裸词。写“用户”就必须写“C端用户”或“后台用户”写“订单”就必须说明是“订单主单”“订单子单”还是“售后订单”。哪怕啰嗦一点也比歧义好。生命周期表则是把每个核心实体的状态、创建时机、销毁时机、核心字段及归属上下文列出来。这个表相当于给整个需求文档做了一张“实体地图”后面不管谁改需求先翻这张表就知道这个实体在哪些上下文里出现过、能不能动它的状态。3. 实操演示用一个订单模块把文档结构搭出来前面说了那么多不演练一遍还是不过瘾。我拿一个最常见的电商订单模块举个例子带你走一遍完整流程看完你就能照搬到自己的项目里。3.1 需求梳理先做状态图还是先写文字我的经验是先跟业务方聊把所有的“他不允许”“他不支持”“他可以”记录下来然后在白板上先画状态图草图画完再补文字说明。反过来写的话文字会把问题藏住状态图则会把问题暴露出来。我们假设现在需要梳理“订单提交后”的全部状态流转。我先在白板上列出粗粒度的状态待支付、已支付、备货中、已发货、已完成、已取消、已退款。事件有这么几个支付成功、超时未支付、取消订单、发货、确认收货、发起退款、退款完成。接下来就是连线找矛盾比如“已发货状态下用户能不能申请退款”如果业务说“可以”那就要再增加一个“售后中”的状态或者一个“退款中”的并行状态。如果没有这一步思考开发到做退款的时候才发现发货后的订单不知道怎么处理又要拉着产品和业务开一轮会。最终整理出的状态机描述放到需求文档里大概是这样的一份描述状态集合: PENDING_PAYMENT, PAID, PICKING, SHIPPED, COMPLETED, CANCELLED, REFUNDED 事件集合: PAY_SUCCESS, PAY_TIMEOUT, USER_CANCEL, START_PICK, SHIP, CONFIRM_RECEIPT, APPLY_REFUND, REFUND_SUCCESS 初始状态: PENDING_PAYMENT 终止状态: COMPLETED, CANCELLED, REFUNDED这样写出来之后开发直接能把它翻译成枚举定义和状态机配置。我在实际操作中会把这张图直接画进文档里不用太复杂画得清楚就行。但需要强调画图的目的不是为了好看而是为了做完整性检查有没有状态既没有入边也没有出边有没有事件被触发后没有对应处理这些在画图阶段解决掉比在开发阶段解决掉便宜得多。3.2 验收标准模板把每个状态转移变成可测试的用例有了状态机的基本骨架之后接下来给每个状态转移写验收标准。我这边的习惯是拿一个表格统一管理而不是在流程描述里东插一条西插一条。表格的格式长这样当前状态触发事件前置条件期望结果动作PENDING_PAYMENTPAY_SUCCESS金额一致订单有效订单状态变为PAID创建支付流水通知备货PENDING_PAYMENTPAY_TIMEOUT超过30分钟未支付订单状态变为CANCELLED释放库存PENDING_PAYMENTUSER_CANCEL用户主动取消订单状态变为CANCELLED若已锁库存则释放PAIDSTART_PICK订单已支付且未被风控拦截订单状态变为PICKING下发仓储系统SHIPPEDCONFIRM_RECEIPT物流已签收或超时默认签收订单状态变为COMPLETED触发评价入口这张表看着简单但它实际上就是TDD里的“测试用例清单”。开发拿到这张表之后照着表里每一行写接口测试、写状态流转测试覆盖得很全。我强烈建议这张表跟需求文档一起评审测试同学参会的时候就直接对照这张表提问题“如果前置条件不满足会怎么办”这类问题非常高效因为问的都是确定性行为不是模糊的业务探讨。实际操作中有些团队还会再往这个表格里加一列“对应代码测试用例ID”等开发完成后回填。这一步看着不起眼长期积累下来的价值非常大因为你手上会有一张“需求条目—状态迁移—测试用例”的完整追溯矩阵改需求的时候只要查这张矩阵就知道哪里要重新测试。3.3 文档上下文结构从顶层到细节的分层组织最后是文档本身怎么组织。我推荐的顺序是这样的业务背景与目标一两段话讲清楚这个模块在整个系统里的位置、要解决什么问题。术语表与上下文边界列出所有核心名词及它们的限定使用范围。总体状态机概览一张大的状态图让读者5秒看懂全貌。状态迁移详表上面说的那张核心表逐条细化。各上下文详述比如用户端流程、后台处理流程、定时任务流程分成子章节每一个子章节内只描述该上下文的行为规则。验收标准集合把每条需求的Given-When-Then断言集中放在这里或者如果有强关联的话直接放在每个子章节末尾。这套顺序的好处是读文档的人可以根据自己的角色选择性地跳到对应的层级去看。老板看第1节架构师看第3、4节开发看第4、5节测试看第6节。每个人都能在5分钟内找到自己关心的内容不会迷路。这比我见过的“把需求写成一个几千行的大叙述文”要高效得多。4. 常见问题与排查技巧实录这套方法在实际落地中的坑方法本身不复杂落地过程中真正的坑一个接一个。我把自己踩过的和自己帮别人排查过的典型问题整理一下给你做个避坑指南。4.1 状态机的粒度失控最常见的问题有两种极端状态太粗或者状态太细。太粗是把“已支付”和“已支付但风控审核中”用一个状态表达背后却藏着两个子状态需求完全看不到。太细是把“订单已创建”“订单信息已校验”“订单价格已计算”“订单库存已锁定”全拆成状态流程被拆得七零八落状态图跟蜘蛛网一样。我自己的判断标准很简单如果两个“状态”的所有行为规则完全一致对外表现也完全一致那它们就是一个状态只是内部的阶段不同。内部阶段不属于状态机的建模范畴需要用流程引擎或子流程来表达不要硬塞进状态机里。这本新人在建模时通常都会踩踩一次就记住了。4.2 验收标准太贴近实现而不是贴近行为用TDD思路写需求的时候很容易写着写着就变成了技术设计文档。比如不是写“系统返回错误码USER_001”而是写“接口在xx条件下返回400”。这个分歧点在于需求文档里的验收标准应该描述“可观测的业务结果”而不是“内部实现细节”。返回码也好、字段结构也好这些都是实现层面的东西你定了反而限制了技术方案的设计空间。正确做法是写“系统拒绝本次请求并返回业务错误提示文案为xxx”至于用400还是200是技术团队的事。当然如果你们的团队已经有成熟的技术约定比如统一返回结构、统一错误码规范那把这个作为背景说明附在文档开头即可不需要每个验收标准都重述一遍。4.3 上下文边界经常被忽略的交叉场景上下文管理做得再好不同上下文之间的数据交互相位接口交互、数据同步、状态回写依然是最容易出bug的地方。我举个真实场景订单模块和库存模块都有自己的状态用户在订单模块支付成功订单状态变成了“已支付”但是库存模块的“锁定库存”没有扣减结果就是订单锁了很多库存但不发货。这种问题说到底是上下文边界没定义清楚——订单状态的迁移不应该只触发订单模块的动作还要触发跨上下文动作。解决方法是在状态机详表里专门增加一列“跨域事件”把状态迁移时需要发出的消息、需要调用的接口、需要写出的数据变更全部列出来。哪怕只列一个接口名都能帮助开发和测试发现问题。我之前在评审一个订单状态表时看到“PAID - PICKING”这次转移的跨域事件没填测试当场问了一句“那仓储系统怎么知道有新订单”这个问题直接就暴露了一个巨大的遗漏。4.4 状态机的可扩展性问题做需求的时候永远要留一个心眼未来这个状态机可能怎么变业务的状态流转往往不是一成不变的比如原本只有“退款”一个终态半年后要接入“退货退款”的流程那原来的状态机该怎么改如果原文档把“已退款”设计成终态强制所有退款汇总成一个状态后面要做“退货退款成功”和“仅退款成功”两种区分就要花很大代价去改表结构和代码逻辑。反之如果设计初期就为“售后状态”预留了独立的子上下文扩展就轻松多了。所以我写文档的时候会在状态机设计说明里加一小段“扩展性备注”把可能的变化趋势写出来并且明确哪些状态是“表面上相同但未来可能分化”的提醒开发和测试在数据模型设计时不要过早合并。这个动作的成本很低收益却极高。4.5 文档更新后没有同步影响分析需求文档最怕的不是写得烂而是写得好好的突然改动关键改动影响到了状态机却没有通知开发。尤其是状态机模型改一个状态迁移往往意味着数据库历史数据要迁移、线上运行中的实例要处理、缓存要清理、消息队列里的积压消息要兼容牵一发而动全身。所以我在团队里定了一个规矩状态机详表每次变更必须附带“影响分析”说明内容包括存量数据的处理方案、运行中流程的兼容策略、对外接口的变化说明、测试回归范围。这个规矩后来帮我避免了好几次线上事故真的非常有必要。最后再分享一个实用技巧我自己的需求文档模板里最后一张表永远叫“需求自检清单”里面只有几个问题这份文档里的每个状态都有明确的入边和出边吗每条状态迁移都有对应的验收标准吗每个核心名词在上下文清单里都有定义吗如果这些问题的答案全是“是”那这份需求文档至少能挡住80%的返工。状态机、TDD、上下文管理说到底不是三个孤立技巧它们共同指向同一件事把模糊的“业务期望”翻译成确定的“系统行为”中间不留模糊地带。还有一个额外的收益你可能没想到用这套方法论写需求文档写完之后你对业务的理解会比业务方还深评审会上你能反向指出业务规则里的逻辑漏洞。那种全场安静、又对又稳的感觉只要你试过一次就再也回不去流水账式的文档写法了。