如何编写高效的软件设计文档How to Write an Effective Software Design Document
一份优秀的软件设计文档能够为团队节省数年的开发时间,并迫使开发者在陷入错误的实现之前深入思考关键决策。它也是协调团队成员和合作伙伴之间设计决策的最佳工具。作者结合自己在 Google、Microsoft 以及自己创业过程中的实践经验,分享了设计文档的编写方法论。掌握这一技能对于提高软件项目成功率和团队协作效率至关重要。
by Michael Lynch, published June 24, 2026
一份优秀的设计文档可以为你节省数年的开发时间。编写设计文档能迫使你在浪费时间去进行错误的实现或陷入死胡同之前,深思熟虑各项重要决策。这也是在团队成员和合作团队之间协调设计决策的最佳方式。
作为曾在 Google、Microsoft 以及我自家公司任职的开发者,我写过不少设计文档。虽然具体细节各有不同,但核心原则始终如一。一份设计文档应该清晰阐述你要解决的难题,并帮助团队成员为你提供反馈。
下面,我将分享自己创建高效设计文档的方法,并说明设计文档中应该包含哪些内容,以及不该包含哪些内容。
设计文档示例🔗
关于设计文档,我最常被问到的问题就是去哪里能找到一份好的设计文档。我从未见过公开的高质量设计文档。我自己写的那些文档都被藏在雇我写它们的公司内部了。
因此,我根据在这里分享的原则,从头开始写了一份设计文档。它详细规划了我正在开发的一款真实 Web 应用程序的设计。
我在编写任何代码之前就创建了这份设计文档,并且在实现该应用程序时一直严格遵循该设计。
这份设计比我平时为个人业余项目写的要详尽得多,但如果我是在专业项目中与其他人协同工作,我创建的设计文档大概也就是这个篇幅和深度。
什么时候应该编写设计文档?🔗
项目越复杂或风险越高,编写设计文档就越有价值。
请考虑以下问题:
如果你对其中任何一个问题的回答为“是”,那么编写设计文档很可能是值得的。如果你对两个或更多问题的回答为“是”,那么这份努力几乎绝对是值得的。
你应该在设计文档上投入多少精力?🔗
设计文档可以是一页简单的摘要,也可以是一份长达 50 页、需要五个不同团队签字批准的文档。你需要自行决定多少细节才是合理的。
关于在设计文档上应该花费多少时间,并没有通用法则,就像没有法则规定你应该对代码进行多少测试一样。正确的投入程度取决于你团队的目标、风险、截止日期和文化。有时,在设计文档上的合理投入甚至可以是零。
设计文档应该包含哪些内容?🔗
如果你在设计文档中详细说明了每一个可能的细节,那么你实际上已经在设计阶段完成了实现工作。这就完全违背了设计文档的初衷。
作为一个经验法则,你可以问一个简单的问题来决定某个决策是否属于设计文档的范畴:如果选错了,代价是什么?
选错的成本是什么?🔗
并非所有的设计决策都同等重要。有些选择的灵活性远超其他选择。
例如,如果你用 C++ 构建了一个 Web 应用程序,而在写了 20 万行代码后发现 Ruby on Rails 是更好的选择,那你就陷入僵局了。从头重写是不可能的,即使你设法在 Rails 中编写新代码,你依然要承受维护两种截然不同的语言代码的负担。
其他一些设计决策则微不足道。例如,如果你的应用要显示 1,000 篇文章的列表,应该一次性全部显示吗?还是应该让用户一次看 20 篇,然后点击“加载更多”来查看接下来的 20 篇?
这无关紧要。
“加载更多”按钮不是设计层面的问题。如果你选择了一种方案,而用户反馈告诉你选错了,你可以在几个小时内修复它。你不需要在设计文档中详述整个思考过程,而且你绝对不应该浪费评审周期去争论这个问题。
设计文档的组成部分🔗
下面,我列出了设计文档中应包含的常见章节。通常你不需要在每份文档中都包含每一个章节。请选择适合你的子集。
标题🔗
你的项目首先需要的是一个标题。这是人们在交谈中提及你的项目的方式,因此请力求具备以下特质:
例如,如果你要在应用服务器和数据库服务器之间添加一个缓存层,RecencyBank 会是一个好名字。它很容易说出口,并且描述了项目的目的。一个糟糕的名字会是“飞行的银马项目”(Project Flying Silver Horse),因为它冗长且毫无意义。
元数据🔗
虽然枯燥但很有用,元数据可以帮助读者了解文档的基本背景:
元数据作者:Michael Lynch (michael@refactoringenglish.com)状态:准备评审创建时间:2026-06-22URL:http://go/recency-bank-design
目标🔗
目标是对项目目的的一句话解释。它应该以任何利益相关者都能理解的通俗语言出现在文档的第一页。
目标通过在 Trogdor Web 服务器和 Postgres 数据库之间添加缓存层来提高应用程序性能。
背景🔗
背景部分解释了项目的背景和动机。它应该回答以下问题:
背景当我们在 2023 年推出 Trogdor Web 应用时,页面通常在 100 毫秒或更短时间内加载完毕。三年后,页面加载中位数时间已激增至 600 毫秒,这使得用户认为我们的应用很迟缓。我们调查了变慢的原因,发现数据库查找占了页面加载时间的 80%。随着我们的数据存储规模越来越大,数据库查找变得越来越慢。我们还发现,95% 的数据库查找都是针对相同的 3% 的数据库行。这种使用模式非常受益于基于内存的缓存。缓存能更快地提供频繁访问的数据,并减少所有其他查询的数据库负载。
你的设计文档在没有外部背景说明的情况下,读起来是否清晰易懂?
想象一下,在队友或合作团队阅读你的设计文档之前,你会对他们说些什么。
现在请意识到,有些读者会在听到你的任何解释之前就看到这份文档,因此他们需要了解的所有内容都应该放在文档的第一页。
如果本项目与其他文档有关联,请附上链接,方便读者查阅。这些文档包括:
相关文档测试计划:http://go/recency-bank-test-planTrogdor 性能报告:http://go/trogdor-perf-2026
目标🔗
目标部分描述的是本项目的高层目标。它应该与背景部分逻辑连贯,并说明在完成实现之后,整体面貌会是怎样的。
避免用实现细节来定义目标。你的目标应该阐明项目如何为用户、团队或公司带来价值。
目标提升 Trogdor Web 应用的用户感知响应速度。降低数据库服务器负载。
非目标🔗
目标定义了项目的范围内内容,而非目标部分则界定了范围外的内容。
有没有哪些目标,读者可能会误以为属于本项目的范围?如果有,请将它们明确列为非目标。
非目标构建通用的、可复用的缓存系统我们为 Trogdor Web 应用添加的缓存层将进行针对应用的优化。在其他系统上复用此缓存不在范围内。基于地理位置感知的缓存未来若能支持部署在地理位置上靠近终端用户的缓存以降低延迟,可能会很有价值,但这不在 v1 的范围内。
场景🔗
如果你的目标类似于"在图表上添加一个'分享为 URL'按钮",读者可能无法理解这在实际中是什么样子。
场景部分可以让你为读者描绘一幅画面,展示完成后的系统在真实世界中是如何运作的。
场景:通过 URL 分享报告Bob 在他的 KeyMetrics 仪表板中创建了一份自定义报告。Bob 导航到菜单栏,点击"分享 > 作为 URL"。Bob 将该 URL 通过邮件发送给队友 Charlie。Charlie 点击链接,以只读模式看到了与 Bob 的报告完全相同的内容。
图表🔗
图表非常有价值,尽管它们看起来可能并非如此。
作为设计作者,你直觉性地理解方案中各个部分如何组合在一起。你脑海中已有架构全貌。而你的评审者并没有这个心理图像,因此让他们最快理解的方式就是画一张图。
展示简单 Web 应用架构的示例图表。
如果你不确定图表中应该包含什么,可以思考以下问题:
选择一个易于编辑的绘图工具。我见过开发者在白板上画了一张精美的图表,然后拍照放到设计文档中。初稿看起来很棒,但之后他们就被这张图永远困住了,因为要修改照片,只能从头重新画起。
Excalidraw、draw.io 和 Google Drawings 都是流行的绘图工具,便于修改。此外,还有 Mermaid、D2 和 Graphviz 等语言,允许你通过编写代码来生成图表。我在使用大语言模型(LLM)为我生成图表代码方面有着很好的体验。请记得附上源图或代码的链接,以便你的团队成员也能复现该图表。
术语表🔗
术语表用于定义读者可能不熟悉的术语。
仔细考虑文档的潜在读者,尤其是新团队成员和直属团队之外的人员。这些读者能看懂文档中提到的内部工具或系统的名称吗?
尽可能使用受众无需查阅术语表就能看懂的术语。在术语表中定义一个词总比不定义好,但最佳的解决方案是使用通俗易懂的术语,或者直接在文中内联解释,这样读者就不必在文档中来回跳转了。
术语表 Apposaurus:团队的内部负载测试工具。我们使用 Apposaurus 来模拟 Trogdor Web 应用的访问量激增,以便验证应用在预期工作负载下能否继续正常运行。Baba-o-styley:执行公司代码风格规范的内部代码检查工具。
约束条件🔗
如果预算、客户、基础设施或依赖项对您的设计施加了重大约束,请解释这些约束,以便读者理解您设计选择的背景。
约束条件 我们的服务器全都是 RISC-V 架构的,因此所有代码和依赖项都必须在 RISC-V 架构上运行。
服务级别目标(SLOs)🔗
SLOs 是服务向其客户或用户提供的可衡量目标。你可能听说过服务级别协议。SLAs 只是 SLOs 加上未达标时的财务罚款。
在公司内部,你通常不会因为同事犯错就对他们进行经济处罚(虽然这听起来可能有点意思?)。因此,设计文档中定义的是 SLOs 而非 SLAs。
SLO 为系统的性能创建了一个可衡量、客观的指标。你的主管可能会告诉你,你的应用必须“在移动端表现出色”,但这太模糊了。你肯定不想等到代码编写完成时才发现,主管对“表现出色”的定义是延迟 <2ms。定义明确的 SLO 能够通过具体、客观的术语来表达目标,从而避免歧义。
关于 SLO,通常需要考虑以下几点:
服务级别目标 Trogdor 面向用户的 HTTP 请求的第 50 百分位延迟:<=200ms Postgres 第 50 百分位查询延迟:<= 80ms
监控 / 告警🔗
一旦确定了(上述)SLO,接下来就该考虑如何在生产环境中对其进行测量了。
验证是否达成 SLO 最简单的方法是手动测试。随着组织的不断成熟,你应该实现监控自动化,以便在 SLO 未达标时立即发现。
在制定监控策略时,请问自己以下几个问题:
监控:以下事件将触发向值班工程师发送寻呼:Trogdor 面向用户的 HTTP 请求的第 95 百分位延迟:>= 3秒;Postgres 服务器在滚动 2 分钟窗口内的平均 CPU 使用率:>= 90%。
时间表🔗
时间表部分将你的项目划分为若干里程碑,并明确了项目利益相关者将在何时收到他们的交付物。
选择能够为利益相关者产生有价值产出物的里程碑。例如,可以从展示模拟数据的 UI 开始,并先将其展示给客户。如果事实证明你误解了客户的需求,假数据能让你及早发现问题,而不是在你已经实现了所有底层管道逻辑以利用生产数据填充 UI 之后才发现。
如果你不知道如何估算项目时间表,我强烈推荐 Joel Spolsky 的《Painless Software Schedules》。这篇文章已有 25 年历史,但它依然是我最喜欢的软件估算策略。
时间表:里程碑 1 (2026-07-01):RecencyBank 在测试环境中上线,包含硬编码的缓存数据子集(不从 Postgres 读取数据)。里程碑 2 (2026-07-17):RecencyBank 在测试环境中上线,并从 Postgres 缓存真实数据。里程碑 3 (2026-08-03):RecencyBank 在测试环境中上线,并执行缓存驱逐和生命周期规则。里程碑 4 (2026-08-22):RecencyBank 完全实现并部署到生产环境。
接口🔗
你的项目旨在为人或其他软件系统提供服务,那么这些交互是什么样的呢?
接口:Trogdor Server 结构体目前直接依赖于 PostgresDB Go 结构体,如下所示:type Server struct { db PostgresDB } PostgresDB 具有以下导出方法:GetUser(id UserID) (User, error) ListUsers() ([]User, error) ... 我们将创建一个 Go 接口类型,其 API 表面与 PostgresDB 相同:type Store interface { GetUser(id UserID) (User, error) ListUsers() ([]User, error) } 我们将实现一个 RecencyBank 缓存类型,它实现了相同的接口并包装了后端 PostgresDB 结构体。RecencyBank 实现将缓存从 Postgres 读取的数据,并在请求改变状态或依赖于不在缓存中的数据时,将请求转发给 Postgres。对 Server 实现的唯一更改是将一个成员的类型替换为新的接口:type Server struct { db store.Store }
依赖项 / 基础设施🔗
依赖项部分应该回答如下问题:
很容易忽视这一部分,但关于语言、库和基础设施的决策会对系统的复杂性和长期维护成本产生重大影响。
深入思考哪些依赖项在实现后难以更改,而对于那些容易替换的依赖项则不必过于担心。更改语言或存储后端很困难,但如果你对用于发送电子邮件的第三方服务不满意,你可以在一个下午就将其替换掉。
依赖项:语言:Go。我们已经广泛使用 Go,并且它是一种适合提供高度并行工作流的语言。第三方包:bbolt:这是一个广泛使用的键值存储实现,它实现了 RecencyBank 所需的许多功能。
安全性🔗
为了构建安全的软件,开发人员必须从设计阶段开始,将安全性贯穿于整个软件生命周期。
安全性部分应该回答以下问题:
即使你认为安全威胁在你的系统中不太可能发生或与之无关,记录你的理由仍然很有帮助。你的解释可能会促使审查者发现你忽略的威胁。
安全性:RecencyBank 不得接受来自公共 Internet 的直接请求,因为它不执行任何访问控制。RecencyBank 将运行在一个隔离的网络中,仅接受来自 Trogdor Web 服务器的入站请求,且只能向 Postgres 服务器池发出出站请求。
隐私🔗
隐私部分提供了一个机会,让你仔细思考系统处理的敏感数据以及为确保其安全将采取的保护措施。它应该回答以下问题:
隐私性:RecencyBank 包含与 Postgres 数据库相同的敏感用户数据,因此它继承了 Postgres 系统的隐私策略。具体而言,工程师只有在关联了错误编号的情况下才能在生产环境中访问 RecencyBank 系统。工程师必须将其访问的用户数据降至调查错误所严格必需的最低限度。
法律考量🔗
如果你的系统在金融或医疗等受到严格监管的领域运行,法律部分将帮助你遵守相关法律。
即使在受监管的领域之外,也要考虑如果出现问题,你的系统是否会触犯法律。说明你将如何避免可能使你的公司或客户面临风险的法律违规行为。
如果你要在开源许可下发布代码,请说明你选择了哪种许可协议以及原因。
FizzleCorp 合同合规性:我们与 FizzleCorp 签订的合同严格限制了为其专有的 FizzlePerfect™ 用户生物特征数据创建新副本的能力。幸运的是,我们的法律团队审查了合同措辞,并确认缓存层符合现有对“存储层”的定义,因此我们可以在 RecencyBank 中缓存 FizzlePefect™ 数据,而无需重新协商合同。
日志记录🔗
在调查错误、性能问题或安全事件时,日志可能具有极大的价值。如果你能在设计时就考虑到有效的日志记录,将使系统的长期维护变得更加容易。
在思考日志记录时,请考虑以下问题:
LoggingRecencyBank 会记录以下事件:在初始化时,记录用于初始化 RecencyBank 的参数,以及主机的 RAM 容量和使用情况。在内存中持久化值失败的情况。在 Postgres 中修改数据后未能使缓存失效的情况。
待解决问题🔗
在撰写设计文档时,你可能会遇到以下至少一种情况:
在设计文档中创建一个名为“待解决问题”(Open Issues)的附录,用来记录你未解决的问题。
待解决问题部分中的每个条目都应说明:
待解决问题:选择缓存的 RAM 大小。我们需要决定为缓存层分配多少 RAM。增加 RAM 可以提高性能,但 RAM 成本高昂,而且额外增加 RAM 的边际效益会递减。在缓存系统和数据库之间,存在一个能使我们的基础设施成本最小化的最佳 RAM 数量。理论上,我们可以通过搭建测试环境并运行多次模拟来找出那个最佳值,但运行这些模拟会耗费我们的开发时间。我估计搭建测试环境和运行单次模拟的成本为 3.0 个开发人天。一旦基础设施搭建完成,每次额外的模拟大约需要 0.75 个开发人天。建议的解决方案:不进行测试,直接选择 128 GB 的 RAM。这应该接近最佳值,而且开发时间的成本远高于 RAM。下一步:征求技术负责人的意见。
已解决问题🔗
当你解决了一个待解决问题时,请总结决策,并将其从“待解决问题”移至设计文档中的“已解决问题”部分。保留完整的讨论记录以备将来参考。
已解决问题:选择缓存的 RAM 大小。决策:为缓存层分配 128 GB 的 RAM。如果我们未能达到性能目标且受限于 RAM,届时我们可以再增加 RAM。运行测试以寻找完美 RAM 大小的开发成本远远超过额外 RAM 的成本。我们需要决定……[此处为原待解决问题的其余部分]
考虑过的替代方案🔗
如果你预料到读者会问“你为什么没有采用 X 方案?”,那么在“考虑过的替代方案”部分主动回答这个问题会很有帮助。在这个部分,你还可以解释你拒绝了哪些方案,特别是那些最初看起来很有吸引力或你进行过深入研究的方案。
我认识一些开发者,他们会花几个小时仔细记录下每一个被否决的设计想法,但我认为这有些小题大做。作为读者和作者,我在替代方案部分所需要的,只是简短的几句话,描述强有力的替代方案以及它们不可行的原因。
考虑过的替代方案:Google Cloud Firestore(持久化存储)其耐用性和可靠性很吸引人,但我不喜欢它的平台锁定以及在本地测试的困难。
推动你的设计文档通过评审🔗
完成设计文档后,下一步就是与你的团队分享并收集反馈。
以下部分涵盖了一些技巧,用于获取有价值的设计反馈,从而推动项目向前发展,而不是让项目在争吵和混乱中停滞不前:
需要完整排版与评论请前往来源站点阅读。