首页 / 资讯中心 / 文章详情

Spec系统:构建需求单一真相源,消除歧义与可追溯

Spec系统:构建需求单一真相源,消除歧义与可追溯 ★ FEATURED ARTICLE
1. 为什么需要“单一真相源”从一次需求撕裂说起做过中大型项目的人大概都经历过这种场面产品经理在需求评审会上说“这个按钮点击后要弹确认框”前端开发记下了后端开发没听到测试同学按自己的理解写了用例。三周后功能上线前端做了弹窗后端接口直接返回成功测试用例里压根没提弹窗这回事。于是线上炸了用户投诉复盘会上大家各执一词谁也说不清“当初到底是怎么定的”。这个场景的根因不是谁不负责而是需求信息没有收敛到一个唯一的、权威的、可追溯的载体上。每个人脑子里都有一份“真相”但这些真相彼此不一致。Spec 系统要解决的就是这个问题——它把散落在聊天记录、会议纪要、口头约定、代码注释里的需求碎片强制收敛成一份单一真相源Source of Truth。所谓单一真相源说白了就是关于“这个系统应该做什么”有且只有一个地方说了算。别的地方可以引用它、讨论它、质疑它但不能绕过它另起炉灶。这个概念在数据工程里很常见比如数据仓库的 ODS 层是唯一数据来源在配置管理里也常见比如 Git 仓库是代码的唯一真相但在需求与规格管理领域很多团队还停留在“文档散落各处、以最新一次口头沟通为准”的原始状态。Spec 系统的核心价值在于三点。第一是消除歧义用结构化、半形式化的语言把需求写死不给“我以为”留空间。第二是可验证每一条规格都能对应到测试用例、代码实现、验收标准形成闭环。第三是可演进需求变了改一处所有引用它的地方自动感知而不是靠人肉同步。这套东西适合谁如果你是小团队做一次性外包项目可能用不上因为沟通成本本来就低。但只要你面对的是多人协作、长周期迭代、需求频繁变更的系统Spec 系统就是刚需。尤其是做平台型产品、基础组件、对外 API 的团队规格一旦模糊下游全是坑。我见过太多团队在“写不写规格”这件事上反复横跳。不写吧扯皮不断写吧觉得浪费时间。问题往往出在他们把规格写成了“文档”而不是“系统”。文档是死的系统是活的。Spec 系统之所以叫“系统”是因为它有一套结构化的语法、明确的层级关系、可执行的校验规则而不是一堆 Word 文档堆在共享盘里。接下来我会从设计思路、核心语法、实操落地、常见坑四个层面把这套东西拆开讲清楚。你不需要是架构师也能看懂因为我会尽量用实际项目里的例子来说明。2. Spec 系统的整体设计与核心思路拆解2.1 为什么是“系统”而不是“文档”先厘清一个概念。很多人一听“规格说明”脑子里浮现的就是一份几十页的 PDF里面写满了“系统应支持用户登录”“系统应保证数据安全”这种废话。这种文档的问题在于它不可执行、不可验证、不可追溯。“应支持用户登录”这句话不同的人能解读出十种不同的行为。Spec 系统的设计哲学完全不同。它要求每一条规格都具备三个属性唯一标识、明确约束、可验证条件。唯一标识让你能引用它明确约束让你知道边界在哪可验证条件让你能判断它是否被满足。举个具体的例子。传统文档写法是“用户登录失败超过 5 次应锁定账户。”这句话看起来没问题但细究起来全是漏洞5 次是连续 5 次还是累计 5 次锁定是锁定多久锁定期间用户看到什么提示管理员能不能手动解锁这些问题不解决开发和测试就得靠猜。Spec 系统的写法会把它拆成一组带标识的规格SPEC-AUTH-001: 用户连续登录失败次数达到 5 次时账户进入锁定状态。 SPEC-AUTH-002: 账户锁定持续时间为 30 分钟从第 5 次失败时间点开始计算。 SPEC-AUTH-003: 锁定期间用户登录请求返回错误码 AUTH_ACCOUNT_LOCKED。 SPEC-AUTH-004: 管理员可通过管理接口手动解除锁定操作记录写入审计日志。每一条都有编号每一条都能单独讨论、单独测试、单独变更。这就是“系统”和“文档”的区别。2.2 核心设计原则GWT 与 SHALL 的分工Spec 系统里有两个关键概念一个是GWTGiven-When-Then一个是SHALL。这两个东西经常被混用但其实分工很明确。SHALL用来表达强制性约束。在规格语言里“SHALL”意味着“必须这样做没有商量余地”。它定义的是系统的行为边界。比如“系统 SHALL 在收到无效 token 时返回 401 状态码”这是一条硬性规定实现必须遵守。与之相对的还有 SHOULD建议、MAY可选但 Spec 系统里最核心的是 SHALL因为它承载了不可妥协的需求。GWT用来表达场景化的验收条件。Given 描述前置状态When 描述触发动作Then 描述预期结果。它把抽象的 SHALL 约束落到具体场景里让测试人员能直接照着写用例。这两者的关系可以这样理解SHALL 是法律条文GWT 是判例。法律条文规定“不得闯红灯”判例则说明“在某某路口、某某时间段、某某车辆闯红灯判定违法”。没有判例法律条文很难执行没有法律条文判例就是一堆散乱的事实。在实际写规格时我通常建议先用 SHALL 把核心约束列出来然后针对每条 SHALL 写至少一个 GWT 场景。如果一个 SHALL 写不出对应的 GWT说明这条约束要么太模糊要么根本不可验证需要重新拆解。2.3 层级结构从领域到规格的收敛路径Spec 系统不是平铺直叙的列表它有明确的层级结构。我习惯把它分成四层层级名称作用示例L1领域Domain划分业务边界认证、订单、支付L2能力Capability描述领域内的功能模块登录、注册、密码重置L3规格Specification定义具体约束登录失败锁定规则L4场景Scenario验收条件连续失败 5 次锁定账户这个结构的价值在于收敛路径清晰。当有人说“登录有问题”时你能快速定位到是哪个领域、哪个能力、哪条规格、哪个场景出了问题。而不是在一堆文档里大海捞针。更重要的是这个结构支持影响分析。当你修改 L3 的某条规格时能顺着层级往上查到它属于哪个能力、哪个领域往下查到它影响哪些场景、哪些测试用例。这种可追溯性在长周期项目里价值巨大。2.4 为什么不用自然语言直接写有人会问用自然语言写清楚不行吗非要搞一套半形式化的语法答案是自然语言适合沟通不适合约束。自然语言有歧义、有省略、有隐含假设。你跟同事说“登录失败几次就锁一下”同事能听懂因为你们有共同上下文。但三个月后新来的开发看到这句话他只能猜。Spec 系统的语法是受控自然语言。它保留了自然语言的可读性但去掉了歧义性。关键词SHALL、GIVEN、WHEN、THEN强制你补全那些平时会省略的信息。写的时候可能觉得啰嗦但读的时候、测试的时候、变更的时候你会感谢当初的啰嗦。我自己的经验是写规格的时间大概能省下三到五倍的返工时间。尤其是需求变更频繁的项目这个比例还会更高。因为每次变更你只需要改规格然后顺着引用关系找到所有受影响的地方而不是靠记忆和搜索去人肉排查。3. 核心细节解析与实操要点3.1 SHALL 语句的写法与常见错误SHALL 语句是 Spec 系统的骨架。写得好整个规格体系清晰可执行写得烂就是一堆正确的废话。先看一个反面例子系统 SHALL 保证用户数据的安全性。这句话的问题在于不可验证。什么叫“保证安全性”加密算不算访问控制算不算审计日志算不算没有明确的验收标准这条规格就是摆设。正确的写法应该把它拆成可验证的约束SPEC-SEC-001: 系统 SHALL 对用户密码使用 bcrypt 算法进行哈希存储cost 参数不低于 12。 SPEC-SEC-002: 系统 SHALL 在传输层使用 TLS 1.2 及以上版本。 SPEC-SEC-003: 系统 SHALL 对敏感字段手机号、身份证号在日志中脱敏输出。每一条都有明确的、可检查的条件。bcrypt cost 不低于 12这是可以检查的TLS 版本可以检查日志脱敏可以检查。这才叫规格。写 SHALL 语句时我总结了几条实操要点一条 SHALL 只表达一个约束。不要把多个约束塞进一句话否则变更时无法单独修改。避免使用“等”“之类”“适当”这类模糊词。这些词在规格里等于没写。明确主体和客体。“系统 SHALL……”比“SHALL……”更清楚因为你知道是谁的责任。尽量量化。“快速响应”改成“响应时间不超过 200ms”“大量数据”改成“单次请求数据量不超过 10MB”。注意SHALL 语句不是越多越好。我见过一个团队写了 800 多条规格结果没人看得完维护成本极高。核心约束写 SHALL次要的用 SHOULD可选的用 MAY分层管理。3.2 GWT 场景的拆解技巧GWT 是把 SHALL 落到实处的关键。一个写得好的 GWT 场景测试人员可以直接拿去做用例开发人员可以直接照着写代码。标准结构是这样的GIVEN: 用户已注册且账户状态正常 WHEN: 用户连续输入错误密码 5 次 THEN: 账户被锁定返回错误码 AUTH_ACCOUNT_LOCKED看起来简单但实际写的时候有几个坑。第一个坑GIVEN 写得太笼统。“用户已注册”不够还要说明账户状态、权限、是否有未完成的操作等。GIVEN 的作用是建立精确的前置状态任何可能影响结果的因素都要写进去。第二个坑WHEN 混入了多个动作。“用户输入错误密码并点击登录按钮”其实是两个动作。严格来说点击登录按钮才是触发点输入密码是前置准备。应该把输入放到 GIVEN 里WHEN 只保留触发动作。第三个坑THEN 只写了成功路径。很多团队写 GWT 只写正常流程异常流程全靠开发自己发挥。结果就是异常处理五花八门。我的做法是每条 SHALL 至少配一个正常场景和一个异常场景。异常场景往往比正常场景更重要因为线上事故大多出在异常路径上。再给一个更复杂的例子GIVEN: 用户已登录账户余额为 100 元目标商品价格为 80 元库存充足 WHEN: 用户提交订单并选择余额支付 THEN: 订单创建成功账户余额扣减至 20 元库存扣减 1 件生成支付流水记录这个场景里GIVEN 把余额、价格、库存都写死了WHEN 只有一个动作THEN 列出了所有可验证的结果。测试人员照着这个写用例基本不会漏。3.3 规格的标识与引用机制Spec 系统里每条规格都要有唯一标识。这个标识不是随便起的它承载了层级信息。我常用的命名规则是SPEC-{领域}-{序号}。比如SPEC-AUTH-001表示认证领域的第一条规格SPEC-ORDER-015表示订单领域的第十五条规格。这个规则的好处是看编号就知道归属。当你在代码注释里看到// 实现 SPEC-AUTH-001你立刻知道这是认证相关的逻辑。当测试用例命名为test_SPEC_AUTH_001_正常锁定你立刻知道它验证的是哪条规格。引用机制同样重要。规格之间会有关联比如订单规格可能引用支付规格。这时候用标识来引用而不是复制粘贴内容。复制粘贴的问题是改了源头副本不会自动更新最后又是一堆不一致的真相。SPEC-ORDER-003: 订单支付 SHALL 调用 SPEC-PAY-001 定义的支付接口。这样写当SPEC-PAY-001变更时所有引用它的地方都能被检索出来影响分析一目了然。3.4 规格与代码、测试的绑定关系Spec 系统如果只停留在文档层面价值会大打折扣。真正的威力在于规格、代码、测试三者绑定。具体怎么做几个实操方法代码注释引用规格编号。在实现某条规格的函数上方写上// SPEC-AUTH-001。这样代码审查时能快速对照规格变更时也能通过搜索找到所有相关代码。测试用例命名包含规格编号。比如test_spec_auth_001_lock_after_5_failures。测试报告一出来哪些规格被覆盖、哪些没覆盖一目了然。CI 流程中加入规格校验。比如检查是否有规格没有对应的测试用例或者是否有代码修改没有更新规格。这个可以用脚本实现成本不高但效果很好。我自己的项目里这三件事是强制执行的。刚开始团队会觉得麻烦但跑了一两个迭代之后大家就体会到好处了再也不用在群里问“这个逻辑当初是怎么定的”直接查规格编号就行。4. 实操过程与核心环节实现4.1 从零搭建 Spec 系统的完整流程假设你现在要在一个已有项目里引入 Spec 系统该从哪里下手我按实际落地顺序拆一遍。第一步划定领域边界。先把系统按业务能力切成若干领域。比如电商系统可以切成用户、商品、订单、支付、物流、售后。领域划分不要太细五到八个比较合适。太细了管理成本高太粗了定位不精准。第二步为每个领域建立规格文件。我习惯用 Markdown 文件一个领域一个文件放在代码仓库的specs/目录下。文件名就是领域名比如specs/auth.md、specs/order.md。放在代码仓库里而不是共享盘里是为了让规格和代码一起版本管理。第三步逐条编写 SHALL 规格。从最核心、最稳定的约束开始写。不要一上来就追求全覆盖先把关键路径的规格写清楚。比如认证领域先写登录、登出、密码重置订单领域先写创建、支付、取消。第四步为每条规格补充 GWT 场景。正常场景和异常场景都要有。异常场景可以参照常见的错误类型参数缺失、权限不足、资源不存在、状态冲突、并发冲突等。第五步建立规格与测试的映射。在测试代码里引用规格编号在 CI 里加一个检查脚本确保每条规格至少有一个测试用例覆盖。第六步建立变更流程。需求变更时先改规格再改代码再改测试。规格变更要经过评审不能随手改。评审的目的是确保变更不会破坏其他规格的一致性。这个流程跑顺之后大概两到三周就能覆盖核心功能。后续就是持续维护和补充。4.2 规格文件的目录结构与模板目录结构我推荐这样组织specs/ ├── README.md # 规格系统说明与索引 ├── auth.md # 认证领域 ├── user.md # 用户领域 ├── order.md # 订单领域 ├── payment.md # 支付领域 └── templates/ └── spec-template.md每个规格文件用统一的模板方便维护和检索。模板大概长这样# 认证领域规格 ## 能力登录 ### SPEC-AUTH-001: 登录失败锁定 **约束** 用户连续登录失败次数达到 5 次时账户进入锁定状态。 **场景 1正常锁定** - GIVEN: 用户已注册账户状态正常当前失败次数为 4 - WHEN: 用户输入错误密码并提交登录 - THEN: 失败次数变为 5账户锁定返回 AUTH_ACCOUNT_LOCKED **场景 2锁定期间登录** - GIVEN: 账户处于锁定状态锁定未过期 - WHEN: 用户输入正确密码并提交登录 - THEN: 登录失败返回 AUTH_ACCOUNT_LOCKED **关联规格** SPEC-AUTH-002, SPEC-AUTH-003这个模板的好处是结构固定信息完整便于机器解析。后续如果想做自动化检查直接按这个格式写解析脚本就行。4.3 参数计算与阈值选择规格里经常涉及阈值比如“失败 5 次锁定”“超时 30 分钟”“分页每页 20 条”。这些数字不是拍脑袋定的需要有依据。以登录失败锁定为例。5 次这个数字怎么来的考虑几个因素用户体验正常用户偶尔输错密码是常事3 次太严格容易误锁。5 次给了足够的容错空间。安全强度暴力破解需要尝试大量组合5 次限制能把成功率压到极低。配合 30 分钟锁定攻击者一天最多尝试 240 次对于强密码来说基本不可能破解。行业惯例大多数主流系统的锁定阈值在 3 到 10 次之间5 次是常见选择。再看超时时间。30 分钟的依据是足够让攻击者放弃又不至于让正常用户等太久。如果业务安全要求更高可以延长到 1 小时如果用户体验优先可以缩短到 15 分钟。这些计算过程应该写在规格的备注里让后来的人知道为什么是这个数字。否则过两年有人觉得“5 次太少了改成 10 次”可能就把安全强度降下来了。提示阈值类参数最好集中管理不要散落在各处。可以在规格文件顶部定义一个参数表下面引用它。这样调整时只改一处。4.4 规格评审的组织方式规格写完不是终点评审才是。评审的目的是在写代码之前把歧义消灭掉。评审怎么组织我的做法是参与者产品、开发、测试三方必须到齐。产品负责确认需求意图开发负责确认可实现性测试负责确认可验证性。频率每个迭代开始前集中评审一次评审下一个迭代要做的规格。不要攒到最后一起评审那样信息量太大效率低。形式逐条过规格重点看 SHALL 是否明确、GWT 是否完整、异常场景是否覆盖。输出评审通过的规格打上“已评审”标记未通过的记录问题并限期修改。评审时最容易发现的问题是产品觉得写清楚了开发和测试理解不一致。这时候当场对齐比等到代码写完再发现要省事得多。我印象最深的一次评审一条关于“订单取消后退款”的规格产品写的是“取消后自动退款”开发理解成“取消后进入退款队列”测试理解成“取消后立即到账”。三种理解三种实现三种测试。评审时当场拆成三条规格取消后触发退款、退款进入异步队列、退款到账时间不超过 24 小时。问题解决。5. 常见问题与排查技巧实录5.1 规格与实现不一致怎么办这是最常见的问题。规格写的是 A代码实现的是 B测试测的是 C。三者不一致线上出问题。排查思路分三步第一步确认哪一个是“真相”。规格是单一真相源所以理论上应该以规格为准。但如果规格本身写错了那就要先改规格。判断标准是规格是否反映了当前的真实需求。如果需求变了但规格没更新那是规格的问题如果需求没变但实现跑偏了那是实现的问题。第二步追溯变更历史。用 Git 查看规格文件和代码文件的修改记录看看是谁先改的、什么时候改的、改了什么。很多时候能直接定位到问题引入的时间点。第三步建立防呆机制。光解决这一次不够要防止下次再发生。我常用的方法是在 CI 里加一个检查如果代码文件修改了但对应的规格文件没修改就发出警告。这个检查不能做到百分之百准确但能拦住大部分明显的遗漏。5.2 规格数量膨胀怎么控制规格写多了维护成本会指数级上升。我见过一个项目写了上千条规格最后没人看得完规格系统形同虚设。控制规格数量的几个方法合并同类项。如果多条规格描述的是同一类约束只是参数不同可以合并成一条带参数的规格。比如“密码长度不少于 8 位”“密码必须包含数字”“密码必须包含字母”可以合并成“密码 SHALL 满足以下复杂度要求长度不少于 8 位包含数字和字母”。分层管理。核心规格必须遵守和参考规格建议遵守分开管理。核心规格控制在 100 条以内参考规格可以多一些但不强制。定期清理。每个大版本迭代时review 一遍规格删除过时的、合并重复的、降级次要的。我的经验值是一个中等规模系统的核心规格在 50 到 150 条之间。超过 200 条就要警惕了要么是系统太复杂需要拆分要么是规格写得太细需要合并。5.3 团队不配合怎么推动引入 Spec 系统最大的阻力往往不是技术而是人。开发觉得浪费时间产品觉得束缚手脚测试觉得多此一举。推动的策略我试过几种最有效的是从小处着手用结果说话。不要一上来就要求全团队写规格。先找一个痛点最明显的模块比如经常出 bug 的支付流程或者扯皮最多的权限逻辑。你一个人先把规格写出来然后在评审会上用规格来对齐需求。当大家发现“这次评审效率高多了”“上线后没出问题”自然就会认可。另一个策略是降低门槛。不要一开始就要求严格的格式和完整的覆盖。先让大家用最简单的模板写哪怕只写 SHALL 不写 GWT 也行。等大家习惯了再逐步提高要求。还有一个技巧是把规格和绩效脱钩。不要规定“每人每周写多少条规格”那样只会催生一堆凑数的垃圾规格。规格的价值在于质量不在于数量。5.4 常见问题速查表问题现象可能原因排查方法解决措施规格与代码不一致变更时未同步对比 Git 修改记录建立 CI 检查机制规格数量失控缺乏合并与清理统计规格总数与覆盖率定期 review 合并团队抵触觉得增加工作量访谈了解具体顾虑从小模块试点规格写得太模糊缺乏写作规范抽查 SHALL 语句培训与模板约束测试覆盖不全规格与测试未绑定检查测试命名强制引用规格编号评审效率低参与者不全或准备不足观察评审过程提前分发材料限时评审5.5 几个我踩过的坑坑一规格写得太早。项目刚立项需求还没想清楚就急着写规格。结果写了改、改了写浪费大量时间。正确做法是等需求相对稳定了再写规格或者先写粗粒度的规格细化留到迭代前。坑二规格写得太细。把实现细节写进规格比如“使用 Redis 缓存”“采用轮询方式”。这些是技术方案不是需求约束。规格应该关注“做什么”而不是“怎么做”。技术方案可以变需求约束相对稳定。坑三规格没有版本管理。规格文件放在共享盘里谁都能改改完也不记录。结果出了问题找不到责任人。规格必须和代码一样纳入版本管理每次修改都有记录、有评审。坑四规格和测试两张皮。规格是规格测试是测试两者没有关联。测试覆盖率看起来很高但覆盖的是不是规格定义的行为没人知道。必须建立映射关系让每条规格都能追溯到测试用例。坑五把规格当成一次性任务。写完就不管了需求变了也不更新。规格变成了历史文档失去了“单一真相源”的意义。规格是活的需要持续维护。我的做法是每个迭代预留一定时间用于规格维护把它当成和写代码同等重要的工作。6. 规格系统的演进与扩展思路6.1 从手工维护到工具辅助刚开始用 Spec 系统时手工维护完全够用。Markdown 文件加 Git成本低、上手快。但当规格数量超过一定规模手工维护就开始吃力了。这时候可以考虑引入工具辅助。几个方向规格解析器把 Markdown 规格解析成结构化数据方便检索和校验。覆盖率检查工具自动检查每条规格是否有对应的测试用例。变更影响分析工具当某条规格变更时自动找出所有引用它的代码和测试。规格生成工具从规格自动生成 API 文档、测试骨架、甚至部分代码。这些工具不需要一开始就全上按需引入即可。我的建议是先跑通流程再考虑工具。流程没跑通就上工具只会把混乱自动化。6.2 规格与 API 文档的联动如果系统对外提供 API规格和 API 文档的联动价值很大。规格定义行为约束API 文档定义接口契约。两者结合能形成完整的对外说明。具体做法是在规格里引用 API 文档的端点在 API 文档里引用规格编号。这样调用方既能知道接口怎么调又能知道接口的行为边界在哪。比如SPEC-API-001: 创建订单接口 SHALL 在库存不足时返回 409 状态码。 关联接口POST /api/v1/orders调用方看到这条规格就知道库存不足时应该处理 409而不是傻等超时。6.3 规格驱动的测试策略规格系统跑顺之后测试策略可以围绕规格来组织。核心思路是每条规格至少一个测试用例每个异常场景至少一个测试用例。测试用例的命名直接引用规格编号测试报告按规格维度统计覆盖率。这样能清楚地看到哪些规格被验证了、哪些没有。更进一步可以做规格驱动的测试生成。对于结构化的 GWT 场景可以自动生成测试骨架测试人员只需要填充具体数据。这能大幅降低写测试的成本。我自己的项目里规格覆盖率是 CI 的必检项。覆盖率低于 90% 直接阻断合并。刚开始团队有怨言但跑了一段时间后大家都认可了这个做法因为线上事故确实少了。6.4 规格系统的长期维护建议Spec 系统不是一劳永逸的它需要长期维护。几个建议每个迭代预留规格维护时间。不要把它当成额外工作而是当成开发流程的一部分。定期做规格健康度检查。检查规格数量、覆盖率、一致性、时效性。建立规格变更的评审机制。规格变更比代码变更影响更大必须经过评审。培养团队的规格意识。新成员入职时规格系统应该是必学内容。最后分享一个我自己的体会Spec 系统的价值不在于规格本身而在于它强迫团队把模糊的需求想清楚。写规格的过程就是消除歧义的过程。很多时候规格还没写完问题就已经暴露出来了。这才是它最大的价值。
阅读完成 · 觉得有帮助?
咨询建站