这份文档是什么
45 讲本身很短,因为它是【收口】——把前面散落的方法论收在一起。
但它收的只是"详细设计"这一层,而完整的流程横跨四讲:17 · 18 讲 需求分析 32 讲 概要设计 45 讲(本讲) 详细设计 42 讲 ——详细设计的一个完整实例(README_IMPL 那四段)所以这份带读做三件事:
① 把四讲拼成一条链(第一、二部分)
② 补上原文没给、但你真要用的时候一定会问的三样(第三部分):
每一步的完成标志是什么?最容易跳过哪一小步?顺序反过来会长出什么?
③ 再上一层,追问这套方法论本身(第四部分):
三段为什么不等重?它比 Google Design Doc 缺了哪两块?什么时候不该用它?
怎么读这份带读
| 这一部分是 | 建议 | |
|---|---|---|
| 第一部分 · 全景(一) | 三个阶段一张表 · 42 讲那四段落在哪一格 | 先读这个 |
| 第二部分 · 原文(二~四) | 45 讲讲了什么,并补上 17/18/32 的对应内容 | 主体 |
| 第三部分 · 引申(五~十) | ⚠ 非原文。完成标志 · 容易跳过的一步 · 反着做的病 · 一张能抄的清单 | 真要动手时看这里 |
| 第四部分 · 再上一层(十一~十八) | ⚠ 非原文。三段不等重 · 举证不是科普 · 跨边界必须显式化 · 业界坐标系与缺的两块 · 文档半衰期 · 适用边界 | 读完前三部分再来 |
只想拿走一样东西 → 直接翻到 第十节那张清单,可以直接抄。
想拿走一条能预测未来的判断 → 第 14.3 节:跨边界 → 隐式的一切必须显式化。
配套脚本
| 脚本 | 讲什么 | 对应 |
|---|---|---|
代码/索引是算法倒推出来的.py |
把 用户故事 → 算法 → 查询清单 → 索引 这条链完整跑一遍,实测四种索引方案的扫描行数差多少;结尾把「跨边界必须显式化」的三个实例并排放 | 4.4 · 十四 |
第一部分 · 全景
一 · 三个阶段,散在四讲里
1.1 一张表拼起来
| 阶段 | 在哪一讲 | 回答什么 | 产出 |
|---|---|---|---|
| ① 需求分析 | 17 · 18 | 谁、在什么场景、为了什么 | 产品定义:元素、边界、与产业上下游的分工 |
| ② 概要设计 | 32 | 整个系统怎么串起来 | 子系统划分 + 核心接口 + 能跑的骨架代码 |
| ③ 详细设计 | 45(42 是实例) | 每个模块具体怎么做 | 现状与需求 / 完备的使用界面 / 数据结构 + 算法 |
原文对这三者关系的定位:
“需求分析并不是纯技术的东西,和编程这件事情无关。它关乎的是用户需求的梳理、产品的清晰定义、可能的演变方向。”
“在概要设计阶段,我们一般以子系统为维度来阐述系统各个角色之间的关系……
我们的焦点是整个系统如何被有效地串联起来。”“详细设计关注的是子系统或模块的全貌。”
★ 三个阶段的粒度是逐级下降的:产品 → 子系统 → 模块。
而每一级都在给下一级定题——这是第五节要展开的重点。
1.2 42 讲那份 README_IMPL 落在哪一格
42 讲的 README_IMPL.md 一共四段,它们全部落在"详细设计"这一格里:
① 逻辑 DOM 结构 ← 45 讲"现状与需求"之后的建模:这个系统里【有什么】
② 使用界面(接口) ← 45 讲第二段
③ 数据结构 ← 45 讲第三段的前一半
④ 实现逻辑(算法) ← 45 讲第三段的后一半
★ 它少了 45 讲的第一段「现状与需求」——因为 qpaint 是从零做的新模块,没有"现状"。
而真实工作里绝大多数是改造存量,那一段反而是最重要的(见 4.1 和第七节)。
★★ 所以:如果你只看过 42 讲那四段,你拿到的是"详细设计"的模板。
上面还有两层:产品定义(做什么/不做什么)和系统串联(怎么跑通)。
跳过那两层直接填这四段,做出来的东西很可能又对又没用。
第二部分 · 三个阶段各自在定什么
二 · 需求分析:定「做什么,以及不做什么」
关键产出是角色 + 用户故事:
“在需求分析阶段,我们关注用户需求的精确表述。我们会引入角色,也就是系统的各类参与方,
以及角色间的交互方式,也就是用户故事。”“需求分析的目标和最终结果,都是要最终形成清晰的产品定义。
产品定义将明确产品的元素,明确产品的边界,与产业上下游、合作伙伴的分工。”
而 32 讲给了这一阶段最重的一句话:
“没有需求分析,就没有业务架构。在业务架构过程中,需求分析至少应该花费三分之一以上的精力。”
★★ 注意"产品定义"三个要素里有两个是在划界:边界、分工。
这一阶段最容易漏的不是"要做什么",是"不做什么"。
明确了边界,后面所有"这个功能要不要加"的争论才有裁决依据;
没有边界,范围就会一直膨胀,而且每次膨胀都显得有道理。
三 · 概要设计:定「怎么串起来」,不是「有哪些模块」
45 讲这段说得很直接,而且反直觉:
“这个阶段我们的核心意图并不是确定系统完整的模块列表,我们的焦点是整个系统如何被有效地串联起来。
如果某个子系统不做进一步的分解也不会在项目上有什么风险,那么我们并不需要在这个阶段对其细化。”
★★ 判据是【风险】,不是【完整】。
只细化那些不细化就有风险的部分——这跟"把所有模块都列全"是两种完全不同的工作。
3.1 ★ 这一阶段必须有代码产出
32 讲的硬要求:
“为了降低风险,系统的概要设计阶段也应该有代码产出。
其一,系统的初始框架代码;其二,原型性的代码来验证。
一些核心子系统在这个阶段提供了 mock 的系统。”
“这样做的好处是,一上来我们就关注了全局系统性风险的消除,
并且给了每个子系统或模块的负责人一个更具象且确定性的认知。
代码即文档。代码是理解一致性更强的文档。”
★★ 这一条最容易被当成"可选项"跳过,但它其实是概要设计的验收标准:
有没有一个能跑通主流程的骨架(哪怕全是假的)。光有架构图和文档,说明"能不能串起来"这件事从来没被验证过——
而这正是概要设计唯一要回答的问题。
3.2 概要设计和详细设计的分工
原文承认两者会重叠,但重心不同:
| 概要设计 | 详细设计 | |
|---|---|---|
| 粒度 | 子系统 | 子系统内部 / 模块 |
| 目标 | 串联整个系统,消除重大风险 | 模块的全貌 |
| 接口 | 只关注最核心的部分 | 接口描述的完备性是必需的 |
| 谁做 | 架构师 | 各个子系统/模块的负责人 |
★ 原文提醒了一句边界:“分解粒度也不能过粗,不应该把特别庞大的子系统直接分出去,
这样项目执行的风险就太高了。”
——也就是说:“不细化"的前提是"它不细化也没风险”,而不是"我懒得细化"。
四 · 详细设计:三段缺一不可
★ 原文开门见山纠了一个常见误解:
“详细设计并不是只谈实现就完事,更不是一个架构图。”
| 段 | 回答 |
|---|---|
| ① 现状与需求 | 现在在哪里,遇到了什么问题,要做何改进 |
| ② 使用界面(接口) | 要做成啥样?——别人怎么用我 |
| ③ 实现原理 | 怎么做到?——数据结构 + 算法 |
4.1 现状与需求:最容易被跳过的一段
“从逻辑自洽的角度,我们任何一篇文档,首先关注的都应该是要解决的问题与目标。”
“现状与需求的陈述,要简明扼要。现状大家都知道,所以不要长篇累牍。
更多的是陈述与我们要做的改变相关的重要事实。”“每个子系统或模块,都有自己的角色分工与用户故事。我们不用重新做一遍需求分析,
但对需求分析的核心结论,在详细设计开始之前需要明确。”
★★ 为什么原文要专门给它单开一节?因为它最常被跳过。
跳过它的后果是:团队不知道你为什么要改,于是评审会变成争论实现细节——
而实现细节是最不该在评审里争的东西。
4.2 使用界面:要详细写,而且要稳定
“使用界面这一部分要详细写,它是团队共识确认的关键。”
“我们的交付物有哪些可执行文件,有哪些包(package)?如果是可执行文件,那么它是一个界面程序,
还是服务?如果是服务,网络协议是什么样的?如果是包,它又包含哪些公开的类或函数。”
两条强调:
“使用界面需要有明确的书写规范。它也是团队共识管理的重要组成,是团队效率、团队默契形成的象征。”
“更需要强调的是,使用界面的稳定是至关重要的。接口的变更需谨慎!”
“对使用界面的不兼容调整,可能出现严重的后果。技术上,可能会导致客户异常,
出现编译失败需要重写代码,或者更严重的是,可能导致他们的系统崩溃。
商业上,则可能导致大量的客户流失。”
★ 这一条和 40 讲第 7.1 节是同一条:已发布的 API 是一份不能撤回的承诺。
4.3 实现 = 数据结构 + 算法
“程序 = 数据结构 + 算法……当我们谈程序的实现时,我们总是从数据结构和算法两个维度去描述它。”
先说数据结构。原文对服务端有一个特别的判断:
“对于服务端程序,数据结构不完全是我们自己能够做主的。数据结构大部分情况下都是基于外存的,
而且有极高的质量要求……存储即数据结构(36 讲)。
所以,服务端程序在数据结构这一点上,最为重要的一件事是选择合适的存储中间件。”
★ 所以服务端的"数据结构"= 表结构设计(mongodb 里叫集合,但惯例仍说"定义表结构")。
描述表结构,四要素:
| # | 要素 |
|---|---|
| 1 | 字段名 |
| 2 | 类型 |
| 3 | 字段含义,以及是否指向另一个表的某个字段 |
| 4 | 索引 |
“你会发现,其实定义表结构和定义内存数据结构本质是完全一致的……
但表结构比内存数据结构多了一个概念:索引。”
4.4 ★★ 索引为什么存在,以及它由谁决定
原文给了两个理由,第二个很关键:
“一方面是因为数据库是泛业务场景的通用数据结构,它是动态的,需要依赖索引来提升数据访问的效率。
另一方面是因为多租户。多租户导致数据量的爆发式增长,导致大部分情况下遍历查找变得不现实。”
而它怎么设计:
“索引怎么设计?它完全取决于算法。算法里面使用了哪些数据访问的特征,
这些数据访问的频次预期是多少,这些决定了我们添加哪些索引是最划算的。”
★★ 这句话把顺序钉死了:索引不是"设计数据库时顺手加的",是【算法要跑哪些查询】倒推出来的。
而算法又来自用户故事——所以完整的推导链是:
用户故事 → 算法(怎么走完这个故事) → 要跑哪些查询 → 加哪些索引反过来先建索引再写算法,你加的索引大概率没用在刀刃上。
(42 讲那两个索引各为了什么,见 带读 16 · 3.3 · 3.4)
4.5 算法 = 用户故事背后的实现机制
原文引了 42 讲那段话,并给了这一课最漂亮的一个定义:
“在架构过程中,需求分析阶段……我们会引入角色,以及用户故事。
到了详细设计阶段,角色和用户故事就变成了子系统、模块、类或者函数的使用界面(接口)。”“所以算法,最直白的含义,指的是【用户故事背后的实现机制】。”
★★ 这句话把"算法"从"排序查找那些"拉回了业务:
一个用户故事 = 一段算法。 不是"这个模块用了什么算法",是"用户想干的这件事,内部怎么走完"。
怎么描述:
| 方式 | 适合什么 |
|---|---|
| UML 时序图 | 流程长、参与方多 |
| 伪代码 | 逻辑复杂时更好——原文举例:“服务端程序对数据库的 SQL 操作往往比较复杂,但从时序图来说流程却并不长,这个时候去画时序图的意义就不大” |
42 讲那份 README_IMPL 的第四段用的就是伪代码。
第三部分 · 引申:真要动手时会问的三件事
⚠ 这一部分不是原文内容。 45 讲讲清了"详细设计包含什么",
但没回答:每一步做到什么程度算完?最容易漏掉哪一小步?顺序反过来会怎样?
这三个问题才是真拿去用的时候会卡住的地方。
五 · ★★ 顺序为什么不能反:四种反着做的病
这个流程最值钱的不是它有哪几步,是【每一步都在给下一步定题】。
需求分析 定了角色和用户故事 → 概要设计只能围绕这些角色划分子系统
概要设计 定了子系统和串联方式 → 详细设计只能在自己这一格里做
逻辑结构 定了有哪些实体 → 接口只能围绕这些实体写
使用界面 定了要支持哪些操作 → ★ 表和索引由"要跑哪些查询"决定
数据结构 定了底层能高效做什么 → 算法只能用存储支持的操作拼出来
反过来做,会长出四种很典型的畸形:
| 反着做 | 长出来的东西 |
|---|---|
| 先写代码,再补需求 | 做出来的东西没人要;或者做了 80% 的功能只有 20% 被用 |
| 跳过"怎么串起来",直接分模块 | 每个模块都很完美,合起来跑不通——接口对不上、时序对不上。这正是 32 讲要求"必须有骨架代码"要防的(3.1) |
| 先选数据库,再定接口 | 接口长得像数据库表的 CRUD,业务语义全漏到调用方那边 |
| 先定实现,再定接口 | 接口暴露实现细节(参数里出现 sql、cursor、redisKey),将来换实现就得改接口——而 4.2 说了"接口的变更需谨慎" |
★★ 第三种最常见,也最难回头。
42 讲那句"使用界面(接口)应该自然体现业务需求"之所以能成立,
正是因为界面是在还没想过数据库的时候定下来的。
六 · ★ 每一步的「完成标志」
怎么知道这一步做完了、可以进下一步? 原文没给,但每一条都能从原文推出来,而且都可验证:
| 阶段 | 做完的标志 | 依据 |
|---|---|---|
| 需求分析 | 你能说清"谁、在什么场景、为了什么",而且能说出不做什么 | 产品定义 = 元素 + 边界 + 分工(第二节) |
| 概要设计 | 有一个能跑通主流程的骨架——哪怕全是 mock | 32 讲:“概要设计阶段也应该有代码产出”(3.1) |
| 详细设计 | 接口完备——不是"核心接口",是全部接口都写出来了 | 45 讲:“接口描述的完备性是必需的”(3.2) |
★★ 这三条都是能拿给别人看的东西,不是"我感觉想清楚了"。
尤其第二条:架构图和文档不算数,要能跑。
七 · ★ 每个阶段最容易被跳过的那一小步
| 阶段 | 最容易跳过 | 后果 |
|---|---|---|
| 需求分析 | “不做什么” | 范围无限膨胀,而且每次膨胀都显得有道理——因为没有裁决依据 |
| 概要设计 | 骨架 / mock 代码 | "能不能串起来"从没被验证过,等到集成才发现对不上 |
| 详细设计 | "现状与需求"那一段 | 团队不知道为什么要改,评审变成争论实现细节 |
★ 三个"最容易跳过"有个共同点:它们都不产出代码,所以看起来"不算干活"。
而它们恰恰是三个阶段各自唯一不可替代的部分。顺带:45 讲专门给"现状与需求"单开一节,32 讲专门强调"概要设计也应该有代码产出"——
原文在这两处的着墨,本身就是在提醒它们最常被跳过。
八 · ★★ 最好的证据:这门课自己走了一遍
26~45 讲这二十讲,就是完整地走了一遍这个流程,而且顺序完全对得上:
26 讲 一上来就是 MVC 分解 + 一个能跑的完整程序 ← 概要设计(含代码产出)
28 讲 离线持久化:先定"对象 ID"这个概念 ← 使用界面先于实现
★ 29 讲 定网络协议 + 做一个 mock 服务端 ← 界面定死了,实现是假的
30 讲 对接,验证串联 ← 概要设计的验收
41 讲 换掉协议层的实现(业务层零改动)
42 讲 README_IMPL 四段 + 换掉业务层的实现 ← 详细设计的完整实例
45 讲 回头讲"详细设计该怎么做" ← 方法论收口
★★ 最有说服力的是:29 讲和 41~42 讲之间隔了十二讲。
网络协议(使用界面)在 29 讲就定死了,那时候服务端还是个 mock;
真正的实现(RPC 框架、mongodb、多租户)到 41~42 讲才做。而 41 讲那次换掉整个 RPC 框架,业务代码一个字节都没改
(见 带读 15 · 1.6)
——这就是"接口先定、实现后换"的回报,也是 4.2 那句"使用界面的稳定是至关重要的"的实证。
九 · ⚠ 一个诚实的补充:它不是瀑布
作者的框架读起来像瀑布模型,但现实中是螺旋的——详细设计做到一半发现问题,会回去改概要设计,甚至回去改需求。
★★ 顺序的意义不是"不许回头",而是:
每次回头都要回到源头改,而不是在下游打补丁。发现接口不好用 → 回去改接口,而不是在调用方加一层 wrapper 绕过去 发现表结构不对 → 回去改表,而不是在算法里写一堆特判 发现需求错了 → 回去改需求,而不是加一个开关兼容两种行为下游补丁的代价是复利的。
42 讲第 2.3 节那个"接口被 uid 污染"就是一次没能回到源头的妥协——
而原文自己很诚实地承认了这一点(见 带读 16 · 2.3)。
十 · 一张可以直接抄的清单
□ ① 需求分析
□ 角色列表(谁会用)
□ 用户故事(每个角色怎么用)
□ ★ 边界:明确【不做】什么
→ 完成标志:能说清"谁在什么场景为了什么",且能说出不做什么
□ ② 概要设计
□ 子系统划分 —— 只细化【不细化就有风险】的部分
□ 子系统之间怎么串(时序、依赖方向)
□ 核心接口(不必完备)
□ ★ 骨架代码 + mock,跑通主流程
→ 完成标志:主流程真的能跑通
□ ③ 详细设计(每个模块一份,照 README_IMPL 抄)
□ 现状与需求 —— 现在在哪、什么问题、要改成什么
□ 逻辑结构 —— 有哪些实体,谁属于谁
□ ★ 使用界面 —— 【完备的】接口 + 约束(含语言表达不了的)
□ 数据结构 —— 字段名 / 类型 / 含义与外键 / ★ 索引(由算法倒推)
□ 算法 —— 每个用户故事怎么走完(伪代码 · 复杂流程用时序图)
→ 完成标志:接口完备,每个用户故事都有对应的伪代码
★ 三条最容易忘的,单独拎出来:
① 需求阶段写"不做什么" ② 概要阶段一定要有能跑的骨架 ③ 索引由算法倒推,不是顺手加的
第四部分 · 再上一层:这套方法论本身经得起推敲吗
⚠ 以下全部是引申,不是原文。
前三部分回答的是「45 讲说了什么、怎么用」。这一部分回答三个更上层的问题:
它为什么长这样?它缺了什么?它在什么情况下会失效?
十一 · ★★ 三段结构的真相:三个读者、三种不可逆
原文把三段并列写了,读起来像是一份提纲的三个小节。但它们在工程上完全不是一个量级的东西。
关键差别是:写错了要付多大代价。
| 段 | 主要读者 | 在跟谁达成什么共识 | 写错的代价 | 半衰期 |
|---|---|---|---|---|
| ① 现状与需求 | 需求方 / 老板 / 跨部门 | 值不值得做 | 白干一个季度 | 项目期 |
| ② 使用界面 | 调用方 / 平级团队 / 客户 | 我给你什么 | ★ 几乎不可逆 | 数年 |
| ③ 实现原理 | 同组同事 / 半年后的自己 | 我怎么做到 | 重构一遍 | 数月 |
★ 第二行那个「几乎不可逆」,就是 40 讲那句 「已发布的 API 是一份不能撤回的承诺」。
★★ 所以三段的投入应该是不对称的 —— 而绝大多数人写反了。
常见的写法是:现状写三页(因为好写)、接口列个大概(因为「代码里有」)、实现画五张架构图(因为显得专业)。
而原文的原话是:现状「要简明扼要」「不要长篇累牍」,「使用界面这一部分要详细写」。
正确的比重是中间那段最厚。
这条还能再推一步:
★ 因为不可逆程度不同,三段的「评审严格度」也应该不同。
① 和 ③ 错了可以改,② 错了改不了。所以评审的时间应该压在 ② 上。
而现实里的评审会往往在 ③ 上吵两小时 —— 吵的是最便宜、最可改的那一段。
11.1 顺带:为什么原文要专门说「不是一个架构图」
“详细设计并不是只谈实现就完事,更不是一个架构图。”
这句话的分量比看起来重。一张框图加箭头,你没法反对它 —— 你说不出它哪里错了,因为它什么都没具体承诺。
而三段结构里的每一段,都可以被人当面反驳:
| 段 | 别人可以怎么反驳你 |
|---|---|
| 现状与需求 | 「这个痛点没你说的那么痛」 |
| 使用界面 | 「这个接口我用不了 / 我这边得改一堆」 |
| 实现原理 | 「这条链路撑不住 / 这个索引没用」 |
★★ 一份好的设计文档的标志,不是「写得全」,是【能被具体地反对】。
这也是"设计的产物是文档而不是图"这个主张的真正理由。
十二 · ★ 概要设计求「通」,详细设计求「全」
原文说两者「会有一定的重叠」,然后说「分工不同,考虑的问题重心不同」。这句话很轻,但它给了一条能直接用的判据:
概要设计 目标 = 消除系统性风险 验收 = 端到端【跑通】一条链路(要有代码)
详细设计 目标 = 能照着它写完代码 验收 = 接口描述的【完备性】
原文的原话对得上:概要设计「不一定会把子系统或模块的完整接口都列出来,实际上它只关注最核心的部分」;详细设计「接口描述的完备性是必需的」。
★★ 同一份东西,两次经过,验收标准换了:第一次求【通】,第二次求【全】。
这解释了一个常见困惑:「概要设计和详细设计不是重复劳动吗?」
不是。第一次是拿代码去证明这条路走得通;第二次是把这条路上的每块砖都编上号。
顺带说,「概要设计阶段也应该有代码产出」(3.1 节)这一条,是这门课里最反直觉、也最值钱的一条。
它把「设计」从文档活动变成了验证活动。业界的同名做法叫 walking skeleton(能跑的骨架)或 tracer bullet(曳光弹,出自《程序员修炼之道》)—— 先打一条从枪口到靶心的完整弹道,哪怕威力为零。
十三 · ★ 「现状与需求」不是背景介绍,是举证
原文有一句被绝大多数人读漏了:
“更多的是陈述与我们要做的改变相关的重要事实,侧重点在于强调这些事实的存在性和重要性。”
「存在性和重要性」——这是法庭用语,不是科普用语。
| 写法 | 它在干什么 |
|---|---|
| ✗「我们的订单系统是这样的:分为 A/B/C 三个模块……」 | 科普。读者本来就知道,白白浪费两页 |
✓「订单查询 P99 是 3.2s,其中 2.8s 花在 order_item 全表扫;上季度因此丢了 4 个大客户」 |
举证。它在证明「这个问题存在,且值得花这笔钱」 |
★★ 现状这一段的功能是:为你后面要花的人力预算,提供证据。
所以判断标准很简单 —— 每写一句现状,问一句「这句支撑了我要做的哪个改动?」支撑不了就删掉。
这也解释了原文为什么反复说「不要长篇累牍」:举证只需要【相关】的事实,不需要【完整】的事实。
十四 · ★★ 全讲最重的一句,以及它背后更大的规律
“索引怎么设计?它完全取决于算法。”
这句话的方向是反的,值得停一下。直觉上是「先有数据结构,再有算法」;这里是**「算法决定数据结构」**。
14.1 为什么会反过来:索引不是数据,是「访问模式」的物化
# 内存里:访问模式【隐含在代码里】,你怎么遍历就怎么遍历
for s in drawing.shapes:
if s.id == target: ... # ← 没有任何人需要提前知道你会这么查
# 外存里:数据库【不知道】你打算怎么访问它
# 所以你必须提前把访问模式【说出来】—— 说出来的那个东西就叫索引
db.shape.ensureIndex({"dgid": 1, "spid": 1})
★★ 索引 = 你对数据库做出的、关于「我将来会怎么查」的一份【声明】。
所以它当然由算法决定 —— 算法就是「我将来会怎么查」本身。
脚本把这条链完整跑了一遍(用户故事 → 算法 → 查询清单 → 索引),并实测了「倒推的索引」和「拍脑袋的索引」差多少:
数据:1000 行(20 张图 × 50 个图形)
索引方案 定位一条 取一批 加权总扫描
──────────────────────────────────────────────────────────────────────
① 倒推出来的 (dgid, spid) 1 行 50 行 5,680 ✓ 最优
② 顺序反过来 (spid, dgid) 1 行 1000 行 100,680 ✗ 慢 18 倍
③ 只想着 id 要唯一 (spid) 20 行 1000 行 113,600 ✗ 慢 20 倍
④ 没建索引 1000 行 1000 行 780,000 ✗ 慢 137 倍
★ 三个方案的字段完全一样,差别只在【顺序】和【组合】。
而决定顺序的,从头到尾只有一样东西:算法要跑哪些查询、各跑多少次。⚠ 一个容易搞混的点:条件都是等值时,
(dgid,spid)和(spid,dgid)对「定位一条」没有区别(数据库会自己调换顺序)。
顺序只在「条件不全」时才起作用 —— 也就是List那个只有dgid的查询。是它把答案钉死的。
14.2 ★ 反过来做会长出什么
假设有人跳过前面几步,直接坐下来设计表:「主键当然是 spid,加个唯一索引」。然后才开始写算法。会长出两件事:
| # | 后果 | 为什么 |
|---|---|---|
| ① | 算法被索引反向绑架 | 因为 (dgid) 单独一个索引也能用,写代码的人会顺手写成「先按 dgid 查出 50 个,再在内存里挑」。★ 这不是他写错了 —— 他是顺着已有的索引写的。 上线后一张图 5000 个图形才暴露 |
| ② | 约束无处安放 | 「同一张图内 ShapeID 唯一」这条约束,唯一索引只能建在 spid 上(挡错了:跨图重名被误拒)或者不建(挡不住)。于是退回应用层 if —— 多机之后失效(见 带读 16 · 3.5) |
★★ 所以 45 讲那条顺序的价值,不是「文档要按这个顺序写」,而是【后一步的正确性依赖前一步的结论】。
索引的顺序是从用户故事的频次里长出来的;跳过用户故事,那个顺序就只能靠猜 —— 而且猜错了当时能跑、测试也过。
14.3 ★★ 再退一步:这是同一条规律的第三次出现
你在 42 讲已经撞见过它两次,只是当时没连起来:
| 场景 | 在「这一侧」是隐式的 | 跨到「那一侧」必须显式 |
|---|---|---|
| 多态序列化 | 内存里对象自己知道自己是 Line |
存成 bson 后类型丢了 → 靠 {"line": {...}} 那个 key 显式标注 |
| 约束 | 单进程里 mutex + if 隐含保证不重复 |
多机后失效 → 必须显式声明唯一索引 |
| 访问模式 | 内存里遍历方式隐含在代码里 | 进了数据库 → 必须显式建索引 |
★★ 规律:凡是跨过一条边界(进程 → 网络、内存 → 外存、我 → 别人),
原本「隐含在上下文里」的东西会全部丢失,必须被显式地重新声明一遍。★★ 而【详细设计文档本身】就是这条规律的又一个实例:
设计隐含在你脑子里,跨到别人那边就丢了,所以必须显式写出来。
45 讲和 42 讲讲的是同一件事 —— 一个在人的层面,一个在代码层面。
这条规律真正的价值是它能【预测】。 下次你要跨一条新边界(单体拆微服务、同步改消息队列、进程内缓存改 Redis),先问一句:「我现在靠哪些隐含的东西活着?」 —— 那些东西,全都要显式化。
十五 · 广度:把这套方法论放进业界的坐标系
这套三段结构不是许式伟发明的,它有一堆同类物。放在一起看,能看出它缺了什么。
| 体系 | 结构 | 特点 |
|---|---|---|
| 许式伟(本讲) | 现状与需求 / 使用界面 / 实现原理 | 接口优先,工程味最重 |
| Google Design Doc | Context and Scope / Goals and Non-Goals / Actual Design / APIs / Alternatives Considered / Cross-cutting concerns | 多了 Non-Goals 和 备选方案 两块 |
| Amazon PR/FAQ(working backwards) | 先写新闻稿和客户 FAQ,再倒推设计 | 从「读者已经拿到成品」往回推 |
| RFC(Rust / IETF) | Motivation / Guide-level / Reference-level / Drawbacks / Rationale and alternatives / Unresolved questions | 面向公开评审,强制列缺点 |
| ADR(Architecture Decision Record) | Context / Decision / Consequences | 极轻,只记一个决策和它的代价,只追加不修改 |
15.1 ★ 对照之下,这一讲明显缺了两块
① Non-Goals(明确不做什么)
原文在需求分析那一讲讲过「划边界」,但详细设计的模板里没有这一格。
而实践中 「这次不做什么」比「这次做什么」更能防止扯皮 —— 因为漏写的事,所有人都默认你会做。
② Alternatives Considered(其他方案,以及为什么不选)
原文通篇没提。但这一块是设计文档里**唯一能防止「三个月后有人问:当初为什么不用 X?」**的东西。
42 讲选 mongodb 那个决定,如果当时写了「为什么不是 MySQL」,今天读起来价值会大得多
(见 带读 16 · 3.2:原文给的理由今天要打折)。
★★ 所以把这一讲的三段扩成五段,可以直接用:
① 现状与需求 举证:问题存在,且值得做 ② 目标与非目标 ← 补 ③ 使用界面 最厚的一段(完备 + 稳定) ④ 实现原理 数据结构 + 算法 ⑤ 备选方案与取舍 ← 补
③ 顺带一个来自 ADR 的启发:ADR 只记一个决策,而且只追加、不修改。
这个形式解决了设计文档最大的病 —— 文档写完就死。两者可以并用:
详细设计文档描述「现在长什么样」,ADR 记录「为什么变成这样」。
十六 · ★ 启发:文档为什么总是烂掉
回到十一节那张表 —— 三段的半衰期差了一个数量级(数年 / 数月)。
★★ 文档腐烂的根因,是把「要活很久的」和「该尽快过期的」写在了同一个文件里。
你去更新它,会发现接口那段还准、实现那段全错了;于是你干脆不更新,整份文档一起死。
对策正好接上 32 讲那句 「代码即文档,代码是理解一致性更强的文档」:
| 内容 | 应该住在哪 | 为什么 |
|---|---|---|
| 使用界面 | 代码外(独立文档 / OpenAPI / proto) | 要被引用、要被追溯、要能对着它谈兼容性 |
| 实现原理 | 尽量住进代码(伪代码 → 真代码,时序图 → 集成测试) | 它变得快,只有可执行的东西不会说谎 |
| 决策理由 | ADR / commit message | 它永远不该被修改,只该被追加 |
★ 42 讲那份
README_IMPL.md之所以好用,就是因为它【薄】—— 薄的东西才会被更新。
十七 · ⚠ 一个诚实的边界:它默认了「问题已知」
这套流程从「现状与需求」开始,默认你已经知道要解决什么问题。它的适用条件是:
✓ 问题已知,解法未知 → 这套流程非常好用(重构、扩容、做一个定义清楚的新模块)
✗ 问题未知,方向也未定 → 这套流程会把你带沟里
为什么会带沟里? 因为详细设计的产物是「共识」,而共识的前提是「有东西可以达成一致」。
探索期你没有那个东西,硬写出来的接口只是把一个未经验证的猜测,固化成了不可撤回的承诺 —— 正好踩中十一节说的最贵的那一格。
★★ 所以:探索期该做的是原型和用户接触(Amazon 的 PR/FAQ 就是干这个的),
等问题稳定了,再进这套流程。
把详细设计用在探索期,等于「用最不可逆的手段,去处理最不确定的东西」。
这一条和第九节那条「它不是瀑布」是配套的:九节说的是「允许回头」,这一节说的是「有些阶段根本不该进来」。
十八 · 一张能带走的表
| # | 判断 | 一句话 |
|---|---|---|
| 1 | 设计的产物是文档,不是图 | 好文档的标志是「能被具体地反对」 |
| 2 | 三段不等重 | 使用界面最厚 —— 它是唯一不可撤回的那段 |
| 3 | 概要 vs 详细 | 概要求「通」(要有代码),详细求「全」(接口完备) |
| 4 | 现状那一段是举证 | 每句都要能支撑某个具体改动,否则删 |
| 5 | 索引完全取决于算法 | 索引是「我将来怎么查」的显式声明 |
| 6 | ★★ 更大的规律 | 跨边界 → 隐式的一切必须显式化(类型 / 约束 / 访问模式 / 设计本身) |
| 7 | 补两段 | Non-Goals 和 Alternatives Considered |
| 8 | 按半衰期分家 | 接口在文档,实现进代码,理由进 ADR |
| 9 | ⚠ 适用边界 | 问题已知、解法未知时才用它 |
验收
| # | 问题 | 答案在 |
|---|---|---|
| 1 | 完整流程分几个阶段?各自在哪一讲?42 讲那份 README_IMPL 落在哪一格? | 一 |
| 2 | 概要设计的核心意图是什么?为什么原文说"不必确定完整的模块列表"? | 三 |
| 3 | 为什么概要设计阶段必须有代码产出?它是这一阶段的什么? | 3.1 |
| 4 | 详细设计三段是什么?为什么原文要专门给第一段单开一节? | 四 · 4.1 · 七 |
| 5 | 索引由谁决定? 完整的推导链是什么? | 4.4 |
| 6 | "算法"在这里的定义是什么? | 4.5 |
| 7 | "先选数据库再定接口"会长出什么样的系统? | 五 |
| 8 | 三个阶段各自的完成标志是什么? | 六 |
| 9 | 这个流程是瀑布吗?顺序的意义到底是什么? | 九 |
| 10 | 详细设计三段的投入应该是均等的吗?哪一段最贵,为什么? | 十一 |
| 11 | 概要设计和详细设计各自的验收标准是什么?一句话概括分工 | 十二 |
| 12 | 「现状与需求」那一段真正的功能是什么?判断一句话该不该留的标准? | 十三 |
| 13 | 为什么内存里没有「索引」这个概念,一到外存就必须有?这条规律你在 42 讲哪两处见过? | 十四 |
| 14 | 对照 Google Design Doc,这一讲缺了哪两块?这套流程什么时候不该用? | 十五 · 十七 |
答案
1. 三个阶段:需求分析(17·18)→ 概要设计(32)→ 详细设计(45),粒度逐级下降:产品 → 子系统 → 模块。
42 讲那四段(逻辑 DOM 结构 / 使用界面 / 数据结构 / 实现逻辑)全部落在"详细设计"这一格,而且少了"现状与需求"那一段(因为 qpaint 是从零做的)。上面还有两层,跳过它们直接填这四段,做出来的东西很可能又对又没用。
2. 核心意图是**“整个系统如何被有效地串联起来”,不是列全模块。所以判据是风险**不是完整:只细化那些"不细化就有风险"的部分。但前提是"它不细化也没风险",而不是"我懒得细化"——原文也提醒了"不应该把特别庞大的子系统直接分出去"。
3. 因为**"能不能串起来"这件事必须被验证**,而架构图验证不了。32 讲要求三样:初始框架代码、原型性验证代码、核心子系统的 mock。它其实是概要设计的验收标准——有没有一个能跑通主流程的骨架(哪怕全是假的)。原文那句"代码即文档,代码是理解一致性更强的文档"说的也是这个。
4. 三段:现状与需求 / 使用界面 / 实现(数据结构 + 算法)。原文开门见山纠了误解:“详细设计并不是只谈实现就完事,更不是一个架构图。”
专门给第一段单开一节,是因为它最常被跳过——跳过的后果是团队不知道为什么要改,评审就变成争论实现细节。
5. 索引完全取决于算法(原文原话)。完整推导链:
用户故事 → 算法(怎么走完这个故事)→ 要跑哪些查询 → 加哪些索引
所以索引不是"设计数据库时顺手加的",是倒推出来的。 反过来先建索引再写算法,加的索引大概率没用在刀刃上。
6. “算法,最直白的含义,指的是【用户故事背后的实现机制】。”
不是"这个模块用了什么排序查找算法",而是"用户想干的这件事,内部怎么走完"。一个用户故事 = 一段算法。
7. 接口会长得像数据库表的 CRUD,业务语义全漏到调用方那边。
因为"使用界面应该自然体现业务需求"这句话能成立,靠的是界面在还没想过数据库的时候就定下来了。
8. 需求分析 = 能说清"谁在什么场景为了什么",且能说出不做什么;概要设计 = 主流程真的能跑通(骨架/mock);详细设计 = 接口完备(不是核心接口,是全部)。三条都是能拿给别人看的东西,不是"我感觉想清楚了"。
9. 不是瀑布,现实中是螺旋的。 但顺序的意义不是"不许回头",而是"每次回头都要回到源头改,而不是在下游打补丁":接口不好用就回去改接口,别在调用方加 wrapper;表结构不对就回去改表,别在算法里写特判。下游补丁的代价是复利的。
10. 不该均等,而且大多数人写反了。 三段的差别是写错了要付多大代价:现状写错 = 白干一季度(可改);实现写错 = 重构一遍(可改);使用界面写错 = 几乎不可逆——40 讲那句"已发布的 API 是不能撤回的承诺"。所以原文说现状"简明扼要"、“使用界面这一部分要详细写”。推论:评审的时间也该压在使用界面上,而现实里评审会常常在最便宜的实现细节上吵两小时。
11. 概要求「通」,详细求「全」。 概要设计验收 = 端到端跑通一条链路(所以必须有代码/mock);详细设计验收 = 接口描述的完备性。所以两者重叠不是重复劳动:第一次是拿代码证明这条路走得通,第二次是把这条路上的每块砖编上号。
12. 它的功能是举证,不是科普——原文原话是"侧重点在于强调这些事实的存在性和重要性",这是法庭用语。它在为你后面要花的人力预算提供证据。判断标准:每写一句现状,问一句"这句支撑了我要做的哪个改动?"支撑不了就删。 这也是为什么原文反复说"不要长篇累牍"——举证只需要相关的事实,不需要完整的事实。
13. 因为索引不是数据,是「访问模式」的物化。内存里访问模式隐含在代码里(你怎么遍历就怎么遍历),而数据库不知道你打算怎么查,所以你必须提前显式声明出来——那个声明就叫索引。
同一条规律在 42 讲出现过两次:① 多态序列化(内存里对象自己知道是 Line,存成 bson 后类型丢了,得靠 key 显式标注);② 约束(单进程 mutex + if 隐含保证不重复,多机后失效,得显式声明唯一索引)。
★★ 规律:跨过一条边界(进程→网络、内存→外存、我→别人),隐含在上下文里的东西全部丢失,必须显式重新声明一遍。而"详细设计文档"本身就是这条规律作用在人身上的结果。
14. 缺 ① Non-Goals(明确不做什么)——漏写的事,所有人都默认你会做;② Alternatives Considered(其他方案为什么不选)——它是唯一能防止"三个月后有人问:当初为什么不用 X"的东西。补上就是五段。
不该用的时候:问题本身还没定下来的探索期。 因为详细设计的产物是共识,而共识的前提是有东西可以达成一致;探索期硬写接口,只是把一个未经验证的猜测固化成了不可撤回的承诺——用最不可逆的手段处理最不确定的东西。
下一步
46 讲:服务端开发篇 · 回顾与总结
回看(这份文档串起来的四讲):
笔记 · 17 讲 需求分析(上) · 18 讲 实战案例(角色 · 用户故事 · 产品定义)
[带读 10 · 32 讲](…/32-架构:系统的概要设计/带读-10-32讲:系统的概要设计(附 AI coding 对照).md)(使用界面 · 稳定点与变化点 · 概要设计为什么要有代码产出)
带读 15 · 41 讲(接口先定、实现后换的回报:换掉整个 RPC 框架,业务零改动)
带读 16 · 42 讲(README_IMPL 四段的完整实例 · 索引为什么是那两个 · 接口被污染的那次妥协)
