← 文章 / 编程开发
Hacker News 4小时前 · 2026-09-14 23:56:28 · 3 阅读

如何撰写高效的软件设计文档

一份优秀的设计文档能为你节省数年的开发时间。编写设计文档会迫使你提前思考关键决策,避免在错误的实现上浪费时间。这也是协调团队成员及合作方在设计方案上达成一致的最佳途径。

我曾在 Google、Microsoft 以及 我自己的公司 以开发者身份撰写过设计文档。虽然具体细节有所不同,但底层原则始终如一。设计文档阐述了你正在解决的难题,并帮助你的团队成员提供反馈。

下文我将分享我创建高效设计文档的方法,并解释设计文档中应该包含什么,不应该包含什么。

设计文档示例🔗

关于设计文档,大家最常问我的问题是:哪里能找到一份写得好的范例。说实话,我从未见过公开的、我认为高质量的设计文档——我自己写的那些都藏在付钱雇我写它们的公司里。

于是,我按照本文分享的原则从头写了一份设计文档,为的是我正在开发的一个真实的 Web 应用

Your browser does not support the video tag.

这份设计文档是在动手写代码之前完成的,目前我在实现应用的过程中也一直在遵循它的设计。

对个人业余项目来说,这份文档比我平时写的要详尽得多,但如果是一个需要与其他人协作的专业项目,我写出来的设计文档差不多就是这个篇幅和深度。

什么时候该写设计文档?🔗

项目越复杂、风险越高,写设计文档的价值就越大。

不妨问自己这些问题:

  • 是否有多人协作来实现这个设计?
  • 项目是否需要超过三个月的全职开发?
  • 实现是否要在生产环境运行多年?
  • 是否涉及跨团队协作?
  • 项目的目标和需求是否模糊?
  • 是否存在可以在设计阶段规避的灾难性风险(如安全漏洞、法律风险)?

只要上面任何一项的回答是"是",就值得花功夫写一份设计文档。如果有两项以上是"是",那设计文档几乎肯定值得写。

设计文档该投入多少精力?🔗

设计文档可以是一页纸的简单说明,也可以是一份需要五个团队审批签字的 50 页文档。具体写到什么程度,需要你自己权衡。

设计文档该写多长,没有定论,就像代码测试写多少也没有统一标准。该投入多少,取决于团队的目标、风险、截止日期以及文化背景。有时候,不写设计文档反而是最优解。

设计文档该包含什么?🔗

如果试图在设计文档里详尽描述每一个可能的细节,那本质上等于在设计阶段就把实现代码写完了,这就违背了设计文档的初衷。

判断一个决策是否该写进设计文档,有个简单的判断标准:判错的代价有多大?

判错的代价是什么?🔗

并非所有设计决策都同等重要,有些选择比其他选择更难以逆转。

比如,如果你用 C++ 开发一个 Web 应用,结果在写完 20 万行代码后才意识到 Ruby on Rails 才是更好的选择,那就麻烦了。从头重写是行不通的,即便你能用 Rails 写新代码,你依然要同时维护两种差异巨大的语言代码。

其他一些设计决策则微不足道。比如,如果应用要展示 100 篇文章,是一次性全显示,还是每次显示 25 篇并让用户点击“加载更多”?

这根本无所谓。

“加载更多”按钮不属于设计层面的问题。如果选了一个方案,用户反馈显示选错了,花几个小时就能修好。设计文档里没必要把整个思考过程都写出来,更不该在评审环节为此浪费宝贵的讨论时间。

设计文档的组成部分🔗

下面列出设计文档中常见的章节。通常不需要每篇文档都包含所有章节,挑出适合你项目的部分即可。

标题🔗

项目首先需要一个好标题。这是大家在沟通中称呼你项目的方式,所以要短、要有辨识度、还要有感染力。

例如,如果你想在应用服务器和数据库服务器之间添加一层缓存,命名为 **RecencyBank** 就很合适。这个名字朗朗上口,且准确反映了项目的目的。而“飞行银马计划”则是一个糟糕的命名,因为它既冗长又毫无意义。

元数据🔗

元数据虽然枯燥,但对帮助读者理解文档的基本背景很有用:

  • 作者是谁?(姓名 + 邮箱地址)
  • 文档创建时间?
  • 权威 URL 是什么?
  • 谁批准了这份文档?批准时间是?
    • 适用于需要队友或合作伙伴签核文档的情况。

元数据

  • URL:http://go/recency-bank-design
  • 作者:Michael Lynch (michael@refactoringenglish.com)
  • 创建日期:2026-06-22
  • 状态:已批准
    • alan@ 签核于 2026-07-14
    • betty@ 签核于 2026-07-15

目标🔗

目标是对项目目的的一句话简述。它应出现在文档的第一页,并使用任何利益相关者都能理解的大白话。

目标

通过在 Trogdor Web 服务器和 Postgres 数据库之间添加缓存层,提升应用性能。

背景🔗

背景部分解释项目的上下文和动机。它应回答以下问题:

  • 为什么团队要启动这个项目?
  • 这个项目解决了什么问题?
  • 之前是否曾尝试过解决该问题?

背景

2023 年 Trogdor Web 应用上线时,页面加载时间通常在 100ms 以内。三年后,页面加载的中位数时间已膨胀至 600ms,导致用户觉得应用运行迟缓。

我们对性能下降进行了调查,发现数据库查询占据了页面加载时间的 80%。随着数据量增长,数据库查询的速度越来越慢。

我们还发现,95% 的数据库查询都集中在相同的 3% 的数据行上。这种访问模式非常适合用内存缓存来优化:缓存可以更快地响应高频数据,同时降低其他查询给数据库带来的压力。

不看背景说明,你的设计文档还能看懂吗?

想象一下,在同事或合作团队阅读文档之前,你会向他们口头交代什么。

注意,有些读者会在没有任何口头说明的情况下直接读文档,所以他们需要了解的背景都应该写在文档第一页

相关文档🔗

如果这个项目与其他文档有关联,要让读者能方便地找到它们。

可以附上以下链接:

  • 项目经理或测试同事为本项目产出的文档(如测试计划、功能规格说明)
  • 相关系统的设计文档
  • 本项目早期版本的设计文档

相关文档

  • 测试计划:http://go/recency-bank-test-plan
  • Trogdor 性能报告:http://go/trogdor-perf-2026

目标🔗

目标部分描述这个项目的高层目标。它应该在逻辑上与背景部分衔接,并说明项目完成落地后世界会是什么样子。

不要把目标写成实现细节。目标应该体现项目能给用户、团队或公司带来什么价值。

反面示例:用内部实现细节来定目标
  • 在基础设施中引入 Kubernetes。
正面示例:用实际影响来定目标
  • 减少因部署新版应用导致的服务中断。

目标

  • 提升 Trogdor Web 应用的用户感知响应速度。
  • 降低数据库服务器负载。

非目标🔗

目标部分界定了项目范围内的内容,非目标部分则说明哪些不在范围内。

是否有读者可能误以为属于本项目范围的目标?如果有,就把它们明确写进非目标部分。

非目标

  • 构建通用可复用的缓存系统
    • 我们为 Trogdor Web 应用添加的缓存层将进行应用层面的优化,将此缓存复用于其他系统不在讨论范围内。
  • 位置感知缓存
    • 未来支持地理位置靠近终端用户的缓存以降低延迟可能很有用,但这不属于 v1 的范围。

场景🔗

如果你的目标是“为图表添加‘共享为 URL’按钮”,读者可能无法理解实际效果是怎样的。

场景部分帮助你向读者描绘系统上线后在真实环境中的运作方式。

场景:通过 URL 共享报告

  1. Bob 在他的 KeyMetrics 仪表板中创建自定义报告。
  2. Bob 导航到菜单栏并点击“共享 > 作为 URL”。
  3. Bob 将该 URL 通过电子邮件发送给同事 Charlie。
  4. Charlie 点击链接,即可看到 Bob 报告的只读副本。

架构图🔗

架构图非常有价值,尽管初看可能并未如此。

作为设计者,你直觉上理解方案各部分的组合方式,脑海中已有架构蓝图。评审者不具备这种心理图景,向他们展示的最快方式就是画图。

Architecture diagram

示例:展示简单 Web 应用架构的图。

如果不确定架构图应包含哪些内容,可以参考以下问题:

  • 数据如何在系统中流动?
  • 系统各组件如何相互协作?
  • 系统如何与其依赖项及下游客户端交互?
  • 系统定义了哪些通信协议?

选择易于编辑的绘图工具。曾见过开发者在白板上绘制精美图表并拍照用于设计文档。初稿看似完美,但照片无法编辑,后续任何修改都需从头重绘,导致被该图长期束缚。

Excalidrawdraw.ioGoogle Drawings 是常用的图表绘制工具,方便修改迭代。还有 MermaidD2Graphviz 等语言支持通过代码生成图表。我曾用 LLM 生成图表代码,体验不错。记得附上源文件或代码链接,方便同事复现图表。

术语表🔗

术语表用于定义读者可能不熟悉的词汇。

认真考虑文档的潜在读者,特别是新入职员工和团队外的相关人员。这些读者能看懂你文中提到的内部工具或系统名称吗?

尽量使用读者熟悉的术语,避免他们查阅术语表。在术语表中定义术语虽然比完全不定义要好,但最佳做法是使用通用术语,或在行内直接定义,以免读者在文档中来回跳转。

术语表

  • Apposaurus:团队内部的压力测试工具。我们用 Apposaurus 模拟大量用户访问 Trogdor Web 应用,以验证其在预期负载下的功能稳定性。
  • Baba-o-styley:内部代码检查器,用于强制执行公司的代码风格规范。

约束条件🔗

如果预算、客户、基础设施或依赖项对设计有重大限制,请明确说明这些约束,帮助读者理解设计选择的背景。

约束条件

我们的服务器均为 RISC-V 架构,因此所有代码和依赖项必须兼容 RISC-V 架构。

服务级别目标(SLO)🔗

SLO 为系统性能提供可衡量的客观指标。你很可能听说过服务级别协议(SLA)。SLA 本质上就是 SLO 加上未达标时的经济处罚条款。

在公司内部,你一般不会因为同事犯错就扣他们的钱(虽然那样可能挺有意思?)。所以设计文档里定义的是 SLO,而不是 SLA。

你的经理可能会说应用必须“在移动端表现流畅”,但这太模糊了。经理心中的“流畅”可能是延迟 <2ms,你肯定不想等到代码写完才发现双方理解不一致。用具体、客观的指标来描述目标,SLO 就能消除这种歧义。

制定 SLO 时通常要考虑以下几个方面:

  • 正常运行时间 / 可用性:系统可用时间占比是多少?
  • 延迟:服务完成请求需要多快?
  • 规模:系统能承受多大的负载?

服务级目标(Service level objectives)

  • Trogdor 面向用户的 HTTP 请求 P50 延迟:<=200ms
  • Postgres P50 查询延迟:<= 80ms

监控 / 告警🔗

确定好 SLO(见上文)后,就该考虑如何在生产环境中衡量这些指标了。

验证是否达到 SLO 最简单的方式是手动测试。但随着组织逐步成熟,应该实现监控自动化,以便第一时间发现 SLO 不达标的情况。

在制定监控策略时,问自己这几个问题:

  • 服务挂了,你怎么知道?
  • 服务性能下降 100 倍,你怎么发现?
  • 还有哪些事件应该触发告警?
    • 比如:CPU 使用率飙升、认证失败、系统错误

监控

以下事件会触发向值班工程师发起呼叫:

  • Trogdor 面向用户的 HTTP 请求 P95 延迟:>= 3s
  • Postgres 服务器过去 2 分钟平均 CPU 使用率:>= 90%

时间线🔗

时间线部分把项目拆分成若干里程碑,明确你在什么时候向项目相关方交付成果。

选择能为利益相关者产生实际成果的里程碑。例如,可以先做一个展示假数据的 UI,并尽早向客户展示。如果发现误解了需求,假数据能让你尽早发现,而不是在已经实现完所有填充生产数据的底层逻辑后才发现。

如果你不知道如何估算项目时间线,我强烈推荐 Joel Spolsky 的 《无痛软件排期》。这篇文章虽然已发表 25 年,但依然是我最喜欢的软件估算策略。

时间线

  • 里程碑 1(2026-07-01):RecencyBank 在测试环境中上线,使用硬编码的缓存数据子集(不读取 Postgres)。
  • 里程碑 2(2026-07-17):RecencyBank 在测试环境中上线,缓存来自 Postgres 的真实数据。
  • 里程碑 3(2026-08-03):RecencyBank 在测试环境中上线,并强制执行缓存淘汰和生命周期规则。
  • 里程碑 4(2026-08-22):RecencyBank 完全实现并部署到生产环境。

接口🔗

你的项目服务于人或其他软件系统,那么这些交互具体是什么样的?

  • 对于图形系统,用户界面是什么?
    • 只需简单的草图;不要纠结于精确的 UI 选择。
  • 对于软件接口,API 或 CLI 的语义是什么?
  • 对于基于文件的接口,文件格式是什么?

接口

Trogdor 的 Server 结构体目前直接依赖一个 PostgresDB Go struct,如下所示:

type Server struct { db PostgresDB }

PostgresDB 拥有以下导出方法:

GetUser(id UserID) (User, error)
ListUsers() ([]User, error)
...

我们将创建一个 Go interface 类型,其 API 表面与 PostgresDB 相同:

type Store interface {
  GetUser(id UserID) (User, error)
  ListUsers() ([]User, error)
}

我们将实现一个 RecencyBank 缓存类型,它实现与现有 interface 相同的接口,并封装后端的 PostgresDB 结构体。RecencyBank 会缓存来自 Postgres 的读取请求,当请求涉及状态变更或依赖缓存中不存在的数据时,将其转发给 Postgres。

Server 实现的唯一变化是将某个成员的类型替换为新的 interface

type Server struct { db store.Store }

依赖与基础设施🔗

依赖部分应回答以下问题:

  • 使用什么编程语言?
  • 代码运行在何种硬件或服务上?
  • 持久化数据存储在哪里?

人们容易忽视这一部分,但关于语言、库和基础设施的决策会显著影响系统的复杂度及长期维护成本。

深入思考哪些依赖在实现后难以更改。不要过于担心那些容易替换的依赖。更改语言或存储后端很困难,但如果你对用于发送邮件的第三方服务不满意,花一个下午就能替换掉。

依赖

  • 语言:Go
    • 我们已广泛使用 Go,且它适合处理高并发的工作流。
  • 第三方包
    • bbolt:这是一个广泛使用的键值存储实现,提供了 RecencyBank 所需的多项功能。

安全性🔗

为了构建安全的软件,开发人员必须将安全融入整个软件生命周期,从设计阶段开始。

安全部分应回答以下问题:

  • 你考虑了哪些威胁?
    • 例如,攻击者尝试所有可能的密码时会发生什么?如果用户上传了感染恶意软件的 PDF 文件呢?
  • 该系统的攻击面有多大?
    • 即,它在哪些地方处理潜在的恶意数据?
  • 信任边界在哪里?
    • 数据何时会从权限较低的系统流向权限较高的系统?
    • 例如,在 Web 应用中,来自用户浏览器的请求就跨越了信任边界,因为 Web 服务器不应假定浏览器传来的输入是安全的。

即使你认为安全威胁在系统中不太可能发生或无关紧要,把你的分析理由写下来仍然有价值。你的说明可能会提醒评审者发现你遗漏的威胁。

安全性

RecencyBank 不得接受来自公网的直接请求,因为它没有实现任何访问控制。

RecencyBank 将部署在隔离网络中,只接受来自 Trogdor Web 服务器的入站请求,且只能向 Postgres 服务器池发起出站请求。

隐私🔗

隐私部分让你有机会梳理系统处理的敏感数据,以及你会采取哪些保护措施。它应回答以下问题:

  • 系统处理哪些敏感数据?
  • 数据会保留多久?
  • 谁有权访问这些数据?
  • 如何保护数据?
    • 例如,静态存储和传输过程中是否都会加密?

隐私

RecencyBank 包含与 Postgres 数据库相同的敏感用户数据,因此沿用 Postgres 系统的隐私政策。具体来说,工程师在生产环境中访问 RecencyBank 系统时必须提供对应的 bug 编号,且访问的用户数据必须严格限于排查该 bug 所需的最小范围。

法律合规🔗

如果你的系统运行在金融、医疗等强监管领域,法律部分能帮助你遵守相关法规。

即使在非监管领域,也要考虑系统一旦出问题是否可能触犯法律。说明你将如何避免可能给公司或客户带来风险的法律违规行为。

如果你打算以开源许可证发布代码,需要说明选择了哪种许可证以及原因。

FizzleCorp 合同合规

我们与 FizzleCorp 的合同严格限制了对其专有的 FizzlePerfect™ 用户生物特征数据进行复制的可能性。

幸运的是,法务团队审查了合同措辞,确认缓存层符合现有“存储层”的定义,因此我们无需重新谈判合同,即可在 RecencyBank 内部缓存 FizzlePerfect™ 数据。

日志🔗

在排查 bug、性能问题或安全事件时,日志往往价值巨大。从设计之初就考虑高效的日志记录,能让系统长期维护变得更加轻松。

在考虑日志设计时,请思考以下问题:

  • 该服务会记录哪些关键事件?
  • 是否有不同的日志级别?
    • 例如:信息、警告、错误、严重
  • 系统将日志存储在哪里?
  • 日志保留多久?
  • 谁有权访问这些日志?
  • 有哪些敏感数据必须排除在日志之外?

日志

RecencyBank 记录以下事件:

  • 初始化时,记录用于初始化 RecencyBank 的参数,以及宿主机上的 RAM 容量和使用情况。
  • 将值持久化到内存时失败。
  • 使缓存失效的操作失败。

未决问题🔗

在撰写设计文档的过程中,你很可能会遇到以下几种情况之一:

  • 设计中存在缺陷,但你不确定如何修复。
  • 设计存在缺口,需要收集更多信息才能填补。
  • 你在多个解决方案之间难以抉择。

在设计文档中创建一个附录,命名为“未决问题”,用来记录这些尚未解决的问题。

“未决问题”部分中的每个条目都应说明:

  • 需要进一步工作的具体问题是什么?
  • 解决该问题有哪些可选方案?
  • 解决该问题的下一步行动是什么?

关于如何在评审阶段管理这些未决问题,请参阅我的配套文章如何高效处理未决问题

未决问题:选择缓存的 RAM 大小

我们需要决定为缓存层分配多少 RAM。增加 RAM 可以提升性能,但 RAM 成本高昂,且额外内存带来的收益是递减的。

缓存系统和数据库之间存在一定的最佳内存分配方案,能让我们将基础设施成本降到最低。理论上,我们可以搭建测试环境、运行多次模拟来找到这个最优值,但这会消耗开发时间。

我估算搭建测试环境并运行单次模拟需要 3.0 个开发日。基础设施就绪后,每次额外的模拟大约需要 0.75 个开发日。

建议方案:不进行测试,直接选择 128 GB 内存。这个数值很可能接近最优,而且开发时间比内存更昂贵。

下一步:请技术负责人评估。

已解决问题🔗

当解决一个开放问题时,请总结决策结果,并将其从设计文档的“开放问题”移至“已解决问题”部分。请保留完整的讨论过程以便日后查阅。

已解决问题:选择缓存的内存大小

决策:为缓存层配置 128 GB 内存。如果我们未能达成性能目标且受限于内存,届时可以增加内存。通过测试寻找完美内存尺寸的开发成本,远超增加内存本身带来的额外成本。

我们需要决定…… [此处填写原开放问题的其余内容]

已考虑的备选方案🔗

如果你预见到读者会问“为什么没采用方案 X?”,在“已考虑的备选方案”部分主动回答会很有帮助。这一部分也是解释你否决的选项的地方,特别是那些起初看似诱人或你曾深入研究的选项。

我知道有些开发者会花数小时仔细记录他们否决的每一个设计想法,但我认为这有点过度了。无论是作为读者还是作者,我只需要在备选方案部分看到几行简要说明,介绍强有力的备选方案以及它们为何不可行。

已考虑的备选方案

  • Google Cloud Firestore(持久存储)
    • 其持久性和可靠性很有吸引力,但我不喜欢平台锁定,且本地测试难度大。

推动设计文档完成评审🔗

设计文档写完之后,下一步就是分享给团队并收集反馈。

下面这一节介绍一些技巧,帮你获得真正有用的设计反馈,推动项目前进,而不是陷入争吵和混乱、原地踏步:

原始来源: Hacker News

评论 (0)