SDD 规格驱动开发:代码写对了,但做的是对的东西吗?
写在前面
上一篇文章 我们聊了 TDD——用测试保证代码写得对。但这只回答了问题的一半。
另一半更致命:你做的东西,是对方想要的吗?
去年做课设的时候我就遇到过。花了两天把一个模块写完了,接口调通了,测试也绿了,拿给队友看。对方沉默了几秒说:「呃……我想要的不是你这样。」
代码没 Bug,测试全覆盖,但不合预期。推倒重来。
这种事的根本原因不是技术问题,是信息不对称——需求和实现之间有一个巨大的空白地带,每个人在这个地带里用自己的理解把空白填满。填出来的东西不一样,等到集成的时候才发现,代价已经付了。
**SDD(Specification-Driven Development,规格驱动开发)**就是在这个空白地带插一面旗子,让所有人都能看见。
SDD 概念篇:把「我以为」换成「我们都同意的这份文档」
什么是 SDD
SDD 的核心思想很简单:在写代码之前,先写一份规格文档,各方认可后再动手。
规格(Specification)不同于需求文档。需求说的是「用户要什么」(what),规格说的是「系统怎么提供」(how)——接口长什么样、数据怎么流转、边界条件怎么处理、错误怎么返回。
打个比方:
| 相当于 | 谁写 | 谁看 | |
|---|---|---|---|
| 需求 | 点菜:「我想吃辣的、不要香菜、15 分钟内上桌」 | PM / 产品 | 所有人 |
| 规格 | 菜谱:「里脊肉 300g 切丝、郫县豆瓣酱 2 勺、大火爆炒 90 秒」 | 开发者 | 开发者 |
| 实现 | 炒菜 | 开发者 | 机器 |
没有菜谱也能炒菜,但换个人炒就不一定是那个味了。没有规格也能写代码,但换个人接手(或者你自己两周后回头看)就不一定知道当时怎么想的了。
SDD 的四个层次
规格不是只有一种写法。根据项目的规模和复杂度,SDD 可以落在不同层次:
1 | 高抽象 低抽象 |
接口协议层——最宏观。前后端之间、服务之间的通信契约。REST API 文档、GraphQL Schema、gRPC 的 .proto 文件都在这个层面。
数据模型层——数据库中流转的数据长什么样。字段类型、约束、默认值、关联关系。
行为合约层——一个函数或模块的契约。输入什么、输出什么、哪些输入会抛异常、异常是什么类型。说白了就是把 TDD 的测试用例用自然语言或类型系统先写一遍,但不直接跑。
算法规格层——最微观。特定算法的计算方式、精度要求、时间复杂度约束。比如排序算法要稳定、浮点精度到小数点后 6 位。
平时说「写个接口文档」「定义一下 schema」其实已经在做 SDD 了,只不过大部分人没有意识到这是一种方法论。
SDD 和 TDD 的关系
这两个 D 不是互斥的,是互补的。一个形象的类比:
SDD 是蓝图,TDD 是验收。
建筑师画完蓝图(规格),施工队盖楼(实现),质检员对照蓝图验收(测试)。你不能跳过蓝图直接找质检员——质检员都不知道标准是什么,怎么验?
同样,你不能只有蓝图没有质检——施工队可能会偷工减料。
实际项目里,SDD 和 TDD 的协作关系是这样的:
1 | 需求分析 |
实践篇:SDD 在前端协作中的落地方式
理论好讲,实践起来要解决的问题是:规格文档放哪?谁来维护?怎么保证不变成一纸空文?
方式一:OpenAPI — 让 API 规格变成活文档
最成熟的 SDD 实践之一就是 OpenAPI(原 Swagger)。核心思路:用一个 YAML 或 JSON 文件描述所有 API 接口,然后从这个文件自动生成文档、类型定义、甚至客户端代码。
1 | # openapi.yaml — API 规格,各方认可的单一真相来源 |
这份文件的威力在于它不是写完了就搁置的静态文档。项目可以围绕它形成一个自动化的工作流:
1 | graph LR |
前后端协作流程变成:
- 后端同学改接口 → 先改
openapi.yaml,提 PR - 前端同学 review 这个 PR → 「这个字段能不能加个
nullable」「这个接口的 query 参数要不要支持分页」 - 双方在规格文件上达成一致 → 后端实现接口,前端并行写界面(可以用 mock server 模拟响应)
- 双方都对照规格验收 → 没有「我以为」「你以为」的空间
这比「后端改完代码,前端发现不对,沟通,重改」的效率高太多了。因为沟通发生在动手之前,而不是集成阶段。
方式二:Protobuf — 服务间通信的强类型规格
如果做的是微服务架构或者 gRPC,Protobuf(.proto 文件)天然就是 SDD 的载体:
1 | // task_service.proto |
.proto 文件的好处是编译期就能发现不兼容变更——你删掉了一个还在用的字段?编译器直接报错,根本到不了运行时。这种「规格即约束」的能力是纯文档做不到的。
方式三:轻量级规格 — 小团队的最小可行 SDD
如果你不开 API、不做微服务、就一个人或者两三个人写全栈,上面这些工具确实有点重。但这不意味着可以跳过规格,只是规格的形式可以更轻量:
1 | ## 任务排序模块规格 |
就写在 Markdown 里,贴在项目 Wiki 上,或者直接放在 docs/specs/ 目录下。关键不是形式,而是「写下来,双方确认,照着实现」这件事本身。
你可能会说这不就是「先想清楚再写代码」吗?对,但大部分人「想清楚了」只是在自己脑子里想,没有落到纸上让对方确认。SDD 的 S 不是 Spec 本身,而是 Spec 作为协作工具这一层含义。
案例实战:没有规格 vs 有规格
说一个真实场景。假设你和另一个同学合作做一个简单的用户信息页,后端负责 API,前端负责页面。
没有 SDD 的剧本
第一天:
- 后端建了一个
/api/user/profile接口,返回{ name, avatar, bio } - 前端调通接口开始写页面,一切顺利
第二天:
- 后端觉得
bio字段应该改叫introduction,改了 - 前端页面崩了,跑来找后端:「bio 怎么没了?」
- 后端:「我觉得 introduction 更准确啊」
- 一番沟通,前端改字段名
第三天:
- 前端需要展示用户加入时间,找后端要字段
- 后端:「啊,我还没存这个数据,得改数据库 schema」
- 后端加了
joinDate字段,返回"2025-09-01" - 前端用
new Date().toLocaleDateString()渲染,在 iOS Safari 上报错——那个日期字符串格式 Safari 不认识 - 前端:「你能不能返回时间戳?」
- 后端:「时间戳没有语义啊,别人调接口怎么看懂」
第四天:
- 聊了半天,最后定下来返回 ISO 8601 格式
- 对齐成本远超编码时间
有 SDD 的剧本
第一天(写规格,不写代码):
后端和前端坐在一起,花了 30 分钟写好 API 规格:
1 | GET /api/user/profile |
双方确认:字段够了、类型清晰、格式统一。
第二天(并行开发):
- 后端实现接口,对照规格返回数据
- 前端用 JSON mock 模拟规格里定义的响应格式,独立开发页面
第三天(集成):
- 前端把 mock 换成真实接口
- 一切正常,因为双方写的都是规格上约定好的东西
- 收工
同样的任务,工时差不多,但后者的摩擦成本是零。 SDD 不是在「写代码」这一步砸时间,而是在「代码写完后互相纠正」这一步省钱。
SDD 不是银弹:什么时候该用,什么时候不该用
| 该用 SDD | 不该用 SDD / 轻量即可 |
|---|---|
| 多人协作,前后端分离 | 一个人全栈,接口自己定自己用 |
| 接口会被外部调用 | 内部临时脚本 |
| 需求稳定,边界清晰 | 快速验证阶段,接口一天改八遍 |
| 微服务,服务间通信 | MVP 原型阶段 |
| 项目需长期维护 | 一次性项目 |
SDD 最大代价不是写规格的体力活,而是冻结得早。需求还在快速变的时候,规格刚写完就过期了,维护规格的成本反而比维护代码高。什么时候启动 SDD 是一个判断力问题——太早是过度设计,太晚是亡羊补牢。
我的经验法则是:当接口开始被别人(或未来的自己)依赖的时候,就值得写规格。 如果只有你一个人用,脑子里记一下就行;如果两个人以上在用,写下来。
总结
TDD 回答「代码写得对不对」,SDD 回答「做的东西对不对」。两者合并在一起,就是一套完整的质量保障线:
1 | SDD(规格) → 定义「正确」的标准 |
当你经历过太多次「做完了但不对」之后,就会发现自己宁愿开工前多花 20 分钟写规格,也不愿意做完后花 2 小时改需求偏差。
下一篇讲 FSD(Feature-Sliced Design)——当项目越来越大、文件越来越多,你是怎么组织前端代码的?按页面分?按技术分?FSD 给了一种完全不同的思路。
系列文章:
- 上一篇:TDD 测试驱动开发 — 测试倒逼实现
- 本篇:SDD 规格驱动开发 — 规格倒逼设计
- 下一篇:FSD(Feature-Sliced Design) — 前端架构方法论
- 后续:DDD(Domain-Driven Design) — 领域驱动设计
