我很怕一种评审现场:一位同事抱着一本80页的《详细设计说明书》进来,目录翻到第三页,就开始讲系统架构图,底下开发听得毫无表情,产品在打哈欠,架构师皱着眉头翻数据库设计。等散会,真正要动手写代码的人跑过来问我:账单核销的状态机到底在哪个服务里更新?我看着他手里的文档,就知道这80页基本白写了。这种情形我看过太多次,核心原因一句话就能讲清:作者没把概要设计和详细设计的边界当回事,把两份文档该管的事混进了一个文档。
做软件项目绕不开这两份设计文档。无论你是项目经理、架构师、开发工程师,还是正在为毕业设计或对外交付项目攒材料,都需要先想清楚它们的区别。很多团队里的文档审查,最后都变成“你写了什么”而不是“这个设计扛不扛得住需求变化”,原因不是大家不认真,而是从一开始就没想清楚手里的文档应该在哪个层面回答问题。这篇文章就把这件事摊开讲:概要设计和详细设计到底差在哪,两份文档的模板结构怎么拆,以及写详细设计时真正好用的技能和工具。你按着这个思路回去改手里的设计文档,评审时被问“这里为什么要这样设计”的概率会小很多。
1. 先认清这两份文档在项目里的真实地位
1.1 设计阶段坐标:概要负责“定骨架”,详细负责“填血肉”
在软件项目里,概要设计和详细设计都处在同一个时间窗口:需求已经冻结,代码还没动工。说得再直白一点,需求分析告诉你“要建一座能容纳一万人的体育场”,概要设计决定“体育场分成观众区、比赛区、疏散通道、后勤区,主入口放在南侧,交通流线怎么走”,详细设计则具体到“这根承重柱用多少号混凝土、直径多大、钢筋怎么绑”。所以概要设计更关心系统的“组织方式”,详细设计更关心“组成元素内部怎么工作”。
我见过不少团队把概要设计当成“需求文档的加长版”,整篇在复述用户故事,反而模块交互、数据分布这些真正属于概要设计的内容被放到“待讨论”里,最后冒出来一份不伦不类的文档。这是个很典型的问题:文档没写错,但站错了位置。站错位置的文档写得越厚,对项目的误导越大。
1.2 两套文档到底差在哪:一张表说清楚
拿登录认证功能举个例子,你会更直观地感受到差别。在概要设计里,你应该看到的是“认证服务负责用户身份校验与令牌签发,用户通过接入层调用认证服务,令牌存储在 Redis 中,会话默认有效期 2 小时”。而在详细设计里,你应该看到的是“LoginController.login(LoginRequest) 的入参出参是什么,登录失败的次数超过 5 次如何处理,Redis key 的命名规则是什么”。
同一个功能,两份文档回答完全不同的问题。很多人混着写,就是因为把“围绕同一个功能”当成了“写同一套内容”。如果把握不住区别,就对照这张表自查:
| 对比维度 | 概要设计 | 详细设计 |
|---|---|---|
| 阅读视角 | 系统级、模块间 | 模块内、代码级 |
| 核心产出 | 架构图、模块划分、接口清单、数据关系、技术选型、非功能策略 | 类设计、方法签名、表结构、流程图、状态机、异常处理、伪代码 |
| 目标读者 | 架构师、项目经理、开发骨干、运维、测试负责人 | 模块开发工程师、测试工程师、未来维护者 |
| 评审关注点 | 模块边界是否清晰、需求是否全覆盖、技术选型是否合理、扩展性如何 | 照着文档能否写代码、边界情况是否想全、是否可测试 |
| 写得太粗的后果 | 后期模块之间扯皮,返工范围跨团队 | 开发中反复补充设计决策,进度失控 |
| 写得太细的后果 | 需求还没验证就固化细节,后续反复改文档 | 文档变成伪代码复述,没人愿意读 |
1.3 为什么现实项目里总是混着写
第一种是“小项目一锅端”。团队小、工期紧,设计阶段压缩成两天,一份文档既讲架构又讲方法,最后只能两头都不讨好。第二种是“文档名叫概要设计,内容却是需求复制品”。整篇都是业务背景和功能列表,评审时没人反对,因为大家都觉得“反正也没信息量”。第三种是“模板混用”,项目组拿了隔壁项目的详细设计模板填概要设计,写着写着就变成了字段级描述。
判断自己有没有混写,方法也很简单:拿三五页文档快速翻一遍,如果每个功能都直接写成“接口怎么调”,却看不到模块和模块之间的职责划分,那这本更接近详细设计;反过来,如果通篇只讲业务场景,代码人员看完还要问“我到底该建几张表”,那它连概要的门都没摸着。
2. 概要设计模板逐段拆解:该写的粗颗粒度决策
2.1 一份不翻车的概要设计目录长什么样
一份标准的概要设计模板通常包含这些部分:
- 引言与背景
- 术语定义
- 总体架构
- 系统功能与模块划分
- 模块间接口与交互
- 核心数据模型与数据存储设计
- 非功能需求(性能、安全、可靠性、可扩展性)
- 部署与运行环境
- 风险分析与设计取舍
按我的习惯,“风险分析与设计取舍”经常被人删掉,但它恰恰最值钱。概要设计阶段最大的价值就是提前暴露“我为了进度砍掉了什么、后续要怎么补”。比如“当前用户中心复用老系统,不做单点登录改造,下一期再迁”,这个记录能避免后期有人拿一个新需求来问“你们当时怎么不考虑这个场景”。没有风险记录的概要设计,像一份没有标注暗礁的航海图。
2.2 架构图、模块边界和接口粒度怎么落笔
总体架构图应该画到什么程度?我的经验是:能看出系统或服务的层次关系、调用方向和依赖方向即可,技术细节标注到关键组件就可以,比如“接入层 Nginx”“缓存 Redis”“消息队列 RabbitMQ”。不要在这里画类图,更不要把某个接口的请求参数表贴进来。架构图是用来讲故事的,不是用来给代码做索引的。
模块划分部分,每个模块要写三件事:职责边界、依赖哪些模块、被谁依赖。能用一句话说清“这个模块管什么、不管什么”的团队,后面写详细设计会顺很多。模块间接口这里只列接口名、方向、触发方式,以及数据概要。“订单模块调用库存模块的扣减库存能力,传入商品编码与数量,预期扣减成功后返回剩余库存”就够了,字段级契约留给详细设计。有人觉得这样太粗,但概要设计本来就不该承担落地细节。
2.3 概要阶段的数据与非功能需求:决策级,不写实现级
概要阶段要不要设计数据库?要,但是设计的是实体和关系,不是建表语句。我在概要设计里会用 ER 图把核心实体画出来,标出关键属性和关系基数,例如“用户 1 对多 订单,订单 多对 1 门店”,至于主键、索引、字段类型,都属于详细设计。如果把 DDL 写到概要设计,后面需求一变,文档痛点会比代码重构还多。
非功能需求这部分最容易被写成口号,比如“系统应保证高并发、高可用”。没有数字的描述等于没有任何约束。我在概要设计里通常会写:“登录接口在单机 4C8G 配置下支持 200 QPS,P99 延迟小于 500ms;核心链路依赖的 Redis 采用主从模式,RTO 目标小于 5 分钟”。这些目标要在概要设计阶段定下来,因为后续编码和压测都拿它当验收标准。概要设计输出的是“决策”,不是“过程”,这是很多人最容易踩的坑。
3. 详细设计模板拆到字段与方法:一次登录模块实例看明白
3.1 详细设计文档的骨架:把“能看懂”推向“能实现”
详细设计模板的常见骨架一般是这么一组内容:
- 模块概述与设计范围
- 功能流程设计(正常流程、异常流程)
- 接口设计(接口清单、方法签名、入参/出参/错误码)
- 数据结构设计(表结构、字段说明、索引、数据量预估)
- 关键设计决策(状态机、并发控制、缓存策略、幂等方案)
- 异常与边界处理
- 安全与性能约束
- 上下游协作点
这个模板最关键的两处在“接口设计”和“关键设计决策”。很多人写详细设计只把 Controller 的方法签名抄一遍,这只能叫“接口登记表”,不能叫设计。真正要写的是“为什么这样设计”。方法名和参数类型是编码时顺手就能定的,但“为什么并发扣减用 Redis 分布式锁而不用数据库悲观锁”这种内容,才是详细设计里别人替代不了的东西。
3.2 一个用户登录模块的详细设计长什么样
我用“用户登录”这个小模块演示一段。模块概述先写清楚:本模块属于认证服务,提供账号密码登录能力,对接接入层、用户服务、Redis 会话存储。然后接口设计如下:
POST /api/v1/login 请求参数: - username string,必填,1~50 字符,允许字母数字下划线 - password string,必填,8~64 字符,传输前使用 RSA 公钥加密 - captchaId string,必填,验证码 ID - captchaCode string,必填,4 位字符验证码 成功响应: { "accessToken": "...", "refreshToken": "...", "expiresIn": 7200 } 主要错误码: 10001 参数格式错误 10002 验证码错误或过期 10003 用户名或密码错误 10004 账号已锁定 10005 账号已被禁用流程设计部分要写清晰:接收请求 → 校验验证码 → 校验参数 → 按 username 查用户表 → 解密密码并比对哈希 → 检查账号状态 → 失败次数超限则锁定 → 生成 token 写入 Redis → 写登录日志 → 返回响应。这里要把“失败次数超限”的条件写清楚:15 分钟内连续错 5 次,锁定 15 分钟,锁定时间由 Redis 过期时间控制。
数据结构部分给出两张表:user 表包含 id、username、password_hash、status、failed_count、locked_until、last_login_at;login_log 表包含 id、username、ip、user_agent、login_time、result。关键设计决策再补三行:密码使用 bcrypt 存储;Redis 里 token 的键是 auth:token:{userId}:{sessionId};过期时间 7200 秒。这样一份详细设计,新来的开发照着就能写,测试也知道造什么数据验证什么场景。
3.3 详细设计和概要设计怎么承接:细化,而不是复制
详细设计里要不要重复概要设计里的模块职责?不要。你只需要在最开头写一句“本模块承接概要设计中的认证服务”,然后把模块边界用一段话带过,剩下的精力全部花在接口契约、数据模型和流程分支上。
最常见的毛病是“概要里说一遍,详细里又说一遍”,两遍内容还一模一样,等于平白多写一半废字。承接的正确做法是逐层放大:概要里有“认证服务负责登录鉴权”,详细里才有上面那份接口定义;概要里有“会话信息存 Redis”,详细里才有 key 命名和过期时间。读者拿两份文档可以一路追下去,这才是它们之间的父子关系。
4. 设计粒度边界:什么时候算“够了”,什么时候在过度设计
4.1 控制粒度的五条经验规则
控制粒度这件事,理论讲再多都不如下面五条规则直接:
- 如果一段内容出现在概要设计里,应该能回答“某个模块或服务该不该存在”,而不是“这个函数怎么写”。
- 如果一段内容出现在详细设计里,应该让开发在写代码时不需要再问产品经理或架构师“这里遇到异常怎么办”“这个字段要不要加索引”。
- 高复杂度、高风险、多分支的场景,详细设计必须细到能用于估算工时,我甚至会写出完整异常码清单。
- 低风险的增删改查页面,详细设计可以只写“接口 + 数据表”两层,不写界面跳转,不写重复的代码结构。
- 判断粗细的终极标准,看“改起来影响多大”:影响范围跨模块,就要在概要里说清楚;影响范围只在函数内部,不要写进文档。
这五条够用。我后面还会讲一条辅助的土办法,用来验收自己写好的文档。
4.2 不同项目规模的粒度速查表
不同规模的项目,设计文档的篇幅和颗粒度差异很大。下面这个表是我的经验值,不是硬性标准:
| 项目类型 | 概要设计参考篇幅 | 详细设计粒度 |
|---|---|---|
| 内部小工具、个人项目 | 5~10 页 | 接口 + 核心流程,关键算法伪代码 |
| 外包、交付型项目 | 15~30 页 | 接口契约 + 表结构 + 关键路径时序图 |
| 中大型单体系统 | 30~50 页 | 类级结构 + 接口 + 状态机 + 异常分支 |
| 微服务、分布式系统 | 40~80 页 | 服务契约 + 领域模型 + 消息格式 + 分布式事务策略 |
注意页数只是经验参考。项目要求“文档覆盖率”“评审签名”时,页数会膨胀,但设计含量并不一定随之增加。任何时候都不要为了凑页数复制代码,评审专家一眼就能看出来。
4.3 从评审里的提问反推文档缺了什么
评审是检验粒度最好的镜子。如果评审时大家反复追问“状态在哪个环节变的”“并发情况下会不会超卖”,说明详细设计里没有把状态机和并发控制写透。如果评审时大家反复追问“这个模块为什么存在”“为什么不用消息队列”,说明概要设计里的模块职责划分和技术选型论证不够。
反过来,如果概要设计评审会上有人要求你把某个私有方法写出来,你可以礼貌地拒绝,那是详细设计的事;如果详细设计评审会上有人问“这个字段默认值多少”而文档里查不到,那是细度出了问题。我判断文档合格有一个很实用的技巧:拿着文档找一个没参与设计开发的同事,让他根据文档描述把功能实现思路完整讲一遍。讲得出来,文档细节够了;讲不出来,哪里缺就补哪里。
5. 写详细设计最实用的skill清单:图、契约与工具
5.1 先画后写:时序图、流程图、状态图分别管什么
详细设计里真正好用的 skill,首先是画图。顺序是:先画三张图再说别的。第一张是时序图,用来表达一个请求跨了哪些模块、调了哪些服务、每一步的返回怎么回来;第二张是状态图,适合表达订单、任务、审批这类有生命周期对象的流转;第三张是流程图,适合表达有大量条件分支的业务逻辑。
工具我常用 Draw.io、PlantUML 或者 ProcessOn,选哪个不重要,但一定要选择“能用文本或可导出文件做版本控制”的,否则图一改,旧版就丢了。很多人一上来就写大段文字,写到一半发现模块调用关系说不清,就是因为时序图没先画。注意,图纸是给人快速建立共识用的,数量要克制,宁可一张图画三遍优化,也不要一口气贴二十张图。
5.2 接口契约要写到什么程度才算“共识”
接口契约写作水平,直接决定详细设计是否可执行。我的接口文档模板包含:接口名与路径、协议与请求方式、入参字段及校验规则、出参结构、错误码列表、权限要求、幂等策略、超时与重试约定、安全要求、依赖的资源。
拿“支付回调”举例,光写“接收支付结果,修改订单状态”等于没说。应该写:入参有 orderNo、channelOrderNo、amount、paymentResult;回调处理时以 channelOrderNo 判重,金额不一致时记录可疑事件并返回失败;接口需要在白名单 IP 范围内调用且验签;处理完成要返回“SUCCESS”给支付渠道,幂等键用 orderNo。到了这个程度,开发和渠道对接人员才能各自开工,而不是互相等。接口契约里的每个字段,至少要能回答“谁传的、什么时候传、取不到怎么办”。
5.3 把文档当代码管:文本化工具与版本管理
文档应该进版本库,跟代码一起管理,而不是散落在 Wiki 或网盘。我建议用 Markdown 写文档,配合 Git 仓库管理,每次评审意见合并到文档的过程就是一次 commit,评审记录和版本 diff 都有据可查。文本化比 Word 更有优势的一点是,它可以在需求或代码变更时被自动 diff 出来,而 Word 很难做到逐行比较。
另一种值得采用的做法是用 OpenAPI 描述对外接口,再用它生成接口文档。这样详细设计里的接口契约可以保持同步,还能直接拿来跑 mock 服务,前端和后端不用等对方写完代码再联调。写详细设计,“让文档活起来”比“写得很厚”有价值得多,这是我认为最值得推荐的工作方式。
5.4 团队模板怎么沉淀而不是摆设
团队沉淀模板不是为了填鸭,而是为了把每次评审的教训固化下来。模板的本质是“上一批人踩过的坑的地图”。我每做完一个项目,会把评审意见里那些“没写清”的地方对应到模板里。例如这次发现“缓存失效策略没人在文档里说清”,那就在模板的“关键设计决策”这一类里加一行“缓存:key 规则、过期时间、失效处理”。
模板一旦固定下来,就不要频繁加章节,否则两三年后模板会变得比项目文档还大,没人愿意填。更好用的做法是团队共用一份设计文档模板,再按项目类型在三个可选项里勾选:单体服务、前端应用、微服务接口。沉淀的是“思考路径”,不是“章节数量”。有人问“代码详细设计的 skill 有哪些好用”,我的回答永远是先掌握控制视角的能力,工具和模板反而是最容易学会的部分。
6. 评审现场翻车记录:文档失败是怎么发生的
6.1 翻车一:100页详细设计,评审时没人想问
一次评审,同事负责的模块写了一份一百多页详细设计,每张页面都有类图和方法签名。评审会上我们安静地翻了二十分钟,没人提问,不是没有疑问,而是信息太多不知道从哪里问起。最后我问了一句:这个模块和订单模块之间的接口,消息是推还是拉?他愣住了,因为文档里根本没写模块间的协作方式。
这就是粒度选错了方向。把力气花在“类图有多完整”,却漏了模块之间的调用方式。那次之后我们立了个规矩:详细设计先写接口契约和状态机,类图只画关键部分,谁都不许把工具自动生成的类图直接贴进来当设计。
6.2 翻车二:概要设计把字段钉死,模块边界却错了
另一个项目,概要设计评审时,架构师把所有下游系统的接口字段都定了。开发照着文档写完后,才发现库存模块和订单模块的边界画反了,本该由订单服务发消息通知库存服务,结果被设计成库存服务定时轮询。因为概要设计里把字段细节钉得太死,大家注意力都在“字段对不对”,反而没人质疑“模块关系对不对”。
这个案例说明,概要设计阶段要克制写细的冲动。字段细节保护不了错误的模块边界,相反,因为太细,评审只会看到叶子,看不到树根。概要设计里的每一页都应该服务于一个目的:让评审者看清这张系统的骨架图,而不是提前陷入实现细节。
6.3 翻车三:没有设计决策记录,三个开发写出三套逻辑
最坑的还不是写太多,而是不记录“为什么这样设计”。同一个模块分配给三个开发同事,有人用乐观锁,有人用悲观锁,有人干脆不加锁,测试说线上出现重复发放,谁都不认为是自己的问题。因为详细设计里只写了“领取红包接口”,没有写并发控制策略和设计依据。
从那以后,我要求详细设计里每个关键决策必须带一句“为什么”,比如“采用 Redis 分布式锁,因为需要跨服务锁;不采用数据库锁,因为会影响该表写入吞吐”。这一句话看似简单,遇到线上问题能省掉一整天的扯皮。设计决策不落纸面,等于把最值钱的经验随手扔掉了。
最后说一条我的土办法:每次写完设计文档,先假想自己是三个月后被临时拉来维护这套代码的同事,脑子里过三个问题:这个模块是谁的职责?它和谁协作,消息走哪个通道?状态什么时候变,变了怎么办?如果文档都能找到答案,我才会把文档发出去评审。设计文档写得粗还是细,没有绝对标尺,但有一个目标从来不偏:让别人在离开文档后,不再需要猜设计意图。你在项目里一次一次试,会慢慢找到自己团队最舒服的粒度。到那时候,概要设计和详细设计这两份文件,就不再是流程负担,而是项目里真正值钱的资产。