埃兹格·迪杰斯特拉的结构化编程思想解释了为何有纪律、简洁的代码在团队、功能和系统增长时仍然能保持正确性与可维护性。

软件很少因为“无法写出来”而失败。它失败是因为一年后,没有人能安全地改动它。
随着代码库变大,每个“微小”的改动都会开始产生涟漪:一个 bug 修复破坏了遥远的功能,一个新需求迫使重写,简单的重构变成一周的协调工作。难点不在于添加代码——而在于在周围一切都变化时保持行为可预测。
埃兹格·迪杰斯特拉认为正确性和简洁性应该是第一等目标,而不是附加项。回报不是空洞的学术论断。当系统更容易被推理时,团队就会减少救火时间,更多精力用于建设。
当人们说软件需要“扩展”时,他们通常指性能。迪杰斯特拉的观点不同:复杂性也会随之扩展。
规模表现为:
结构化编程不是为了刻板而刻板。它是选择使人容易回答两个问题的控制流与分解方式:
当行为可预测时,变更变成了例行而非冒险。这就是迪杰斯特拉仍然重要的原因:他的纪律针对的是增长中软件的真实瓶颈——能否足够理解它以便改进它。
Edsger W. Dijkstra (1930–2002) 是荷兰计算机科学家,影响了程序员构建可靠软件的思维方式。他参与早期操作系统工作、为算法做出贡献(包括以他命名的最短路径算法),更重要的是,他推动了一个观点:编程应该是我们可以推理的事情,而不是反复尝试直到看起来可行。
迪杰斯特拉更关心的是我们是否能解释为什么程序在重要情况下是正确的,而不是仅仅让程序在几个例子上输出正确结果。
如果你能陈述一段代码应该做什么,你就该能逐步论证它确实如此。这个心态自然而然地导向更容易阅读、更容易审查、而不太依赖英雄式调试的代码。
迪杰斯特拉的文字有时显得不妥协。他批评“聪明”的技巧、混乱的控制流和使推理困难的编码习惯。严格并非为了样式警察,而是为了减少歧义。当代码的含义清晰时,你花在意图争论上的时间更少,把时间用在验证行为上。
结构化编程是用一小组明确的控制结构——顺序、选择(if/else)和迭代(循环)来构建程序,而不是纠缠的跳转流。目标很简单:让程序路径可理解,从而能自信地解释、维护和改变它。
人们常把软件质量描述为“快”、“漂亮”或“功能丰富”。用户体验正确性的方式不同:那是一种安静的信心,认为应用不会给他们带来惊讶。当正确性存在时,没有人会注意到它;当它缺失时,其他一切都不再重要。
“现在能用”通常意味着你尝试了几个路径并得到期望结果。“能一直用”意味着在时间、边界情况和变更后仍按意图表现——重构后、整合后、更高流量和新团队成员改动后仍如此。
一个功能可以“现在能用”但仍然脆弱:
正确性是关于消除这些隐含假设——或把它们明确化。
一个小 bug 很少会一直只是小范围的。一个不正确的状态、一个越界的 off-by-one 或一条不明确的错误处理规则会被复制到新模块,被其他服务包裹,被缓存、被重试或被“绕开”。随着时间推移,团队不再问“什么是真的?”,而开始问“通常发生什么?”这时事故响应变成了考古学。
乘数效应来自依赖:一个小的异常行为变成许多下游异常行为,每个都有自己的部分修复。
清晰的代码通过改善沟通来提升正确性:
正确性意味着:对于我们声称支持的输入与情况,系统稳定地产生我们承诺的结果——在无法满足时以可预测、可解释的方式失败。
简洁不是让代码“可爱”、极简或聪明。它是让行为易于预测、解释和在不惧怕的情况下修改。迪杰斯特拉重视简洁,因为它增强了我们推理程序的能力——尤其是在代码库和团队增长时。
简洁的代码同时维持少量想法:清晰的数据流、清晰的控制流和明确的职责。它不会迫使读者在脑中模拟许多替代路径。
简洁不是:
许多系统之所以难以改变,并非因为领域本身固有复杂,而是因为我们引入了偶然复杂度:交互的旗标、从未移除的特殊补丁和主要为绕过早期决策而存在的层。
每一个额外的例外都是理解的税负。成本会在后来显现:当某人尝试修复 bug 时,发现一个区域的改动会微妙地破坏几个其他区域。
当设计简单时,进展来自稳定工作:可审阅的改动、更小的 diff、更少的紧急修复。团队不再需要记住每个历史边界情况的“英雄”开发者,或在凌晨两点能在压力下调试的人。系统支持正常的人类注意力跨度。
实用测试:如果你不断添加例外(“除非……”、“只有为这个客户……”),你很可能在累积偶然复杂度。优先选择减少行为分支的解决方案——一条一致规则胜过五个特殊情况,即便这条一致规则稍微比最初想象的更泛化。
结构化编程是一个简单但后果重大的思想:写代码使其执行路径容易跟随。通俗来说,大多数程序可以由三种构建块构成——顺序、选择 和 重复——而无需依赖纠结的跳转。
if/else、switch)。for、while)。当控制流由这些结构组装时,通常可以自上而下解释程序做什么,而无需在文件间“瞬移”。
在结构化编程成为常态之前,很多代码库大量依赖任意跳转(经典的 goto 风格)。问题不是跳转总是邪恶,而是不受限制的跳转制造了难以预测的执行路径。你会不断问“我们怎么到这里?”和“这个变量是什么状态?”,而代码不会清楚地回答。
清晰的控制流帮助人类构建正确的心理模型。调试、审查 PR 或在时间压力下改变行为时,大家依赖的就是这个模型。
当结构一致时,修改更安全:你可以改动一个分支而不会无意影响另一个,或重构循环而不会错过隐藏的退出路径。可读性不仅仅是审美——它是自信变更行为而不破坏已有功能的基础。
迪杰斯特拉推动了一个简单思想:如果你能解释为什么代码是正确的,你就能更少恐惧地改变它。三个小的推理工具让这变得可行——无需把团队变成数学家。
不变式 是在代码运行期间(尤其在循环内)始终为真的事实。
示例:你在汇总购物车里价格。一个有用的不变式是:“total 等于到目前为止已处理商品的和。” 如果在每一步都保持这一点,那么当循环结束时,结果是可信的。
不变式很强大,因为它把注意力放在永远不能被破坏的东西,而不是仅仅下一步应该发生什么。
前置条件 是函数运行前必须为真的条件。后置条件 是函数结束后保证的事项。
日常例子:
在代码中,前置条件可能是“输入列表已排序”,后置条件可能是“输出列表已排序,并包含相同元素外加插入项”。
即便是非正式地写下这些,设计也会更清晰:你决定函数期望什么和承诺什么,自然会把它写得更小、更专注。
在审查中,讨论会从样式上的“我会这样写”转向正确性的辩论:“这段代码保持了不变式吗?”“我们是要强制前置条件还是把它记录下来?”
你不需要形式证明就能受益。挑出最常出 bug 的循环或最棘手的状态更新,在上面加一句不变式注释。后来有人修改代码时,那条注释像护栏一样:若改动破坏了这一事实,代码就不再安全。
测试与推理追求相同的目标——软件按意图工作——但方式不同。测试通过示例发现问题。推理通过使逻辑明确来防止某些类别的问题。
测试是实际的安全网。它们抓住回归、验证真实场景,并以团队可运行的方式记录期望行为。
但测试只能显示 bug 的存在,而不能表明其不存在。没有测试集能覆盖所有输入、所有时序变化或所有特性间的交互。许多“在我机器上能跑”的失败来自未测试的组合:罕见输入、特定的操作顺序或只有在若干步骤后才出现的微妙状态。
推理是证明代码属性:“这个循环总会终止”、“这个变量从不为负”、“这个函数不会返回无效对象”。做好了,它能排除整类缺陷——尤其是边界和极端情况相关的缺陷。
限制在于成本和范围。对整个产品做完全形式化证明通常不经济。推理在有选择地应用时效果最好:核心算法、对安全敏感的流程、金钱/计费逻辑和并发部分。
广泛使用测试,并在失效代价高的地方应用更深的推理。
一个介于两者之间的实用桥梁是把意图可执行化:
这些技术不会取代测试——但会收紧安全网,把模糊的期望变成可检查的规则,使得编写错误更难、诊断更容易。
“聪明”的代码在当下常常感觉是一种胜利:更少行、巧妙技巧、一行把事情做完让你感觉很聪明。问题是聪明不会随着时间或人员扩展。六个月后,作者忘记了技巧。新队员按字面读懂,却错过了隐含假设,从而以破坏行为的方式改动它。这就是“聪明债”:短期的速度换来长期的混乱。
迪杰斯特拉的观点不是“写无聊代码”作为风格偏好——而是有纪律的约束使程序更容易被推理。在团队里,约束还减少决策疲劳。如果每个人都知道默认做法(命名、函数结构、什么算“完成”),你就不用在每个 PR 里重复讨论基础问题。那时间可以回到产品工作上。
纪律体现在常规实践中:
一些具体习惯可防止聪明债积累:
calculate_total(),不要 do_it())。纪律不是追求完美——而是让下一个改动可预测。
模块化不仅仅是“把代码拆成文件”。它是把决策隔离在清晰边界后面,使系统其余部分无需知道(或关心)内部细节。模块隐藏混乱部分——数据结构、边界情况、性能技巧——只暴露小而稳定的接口面。
当变更请求到来时,理想结果是:改动只影响一个模块,其余保持不动。这就是“让变更局部化”的实际含义。边界防止意外耦合——更新一个特性却悄悄破坏三个其他特性的情况。
一个好的边界也让推理变容易。如果你能陈述模块保证什么,你就能在不重读其整个实现的情况下推理更大的程序行为。
接口就是承诺:“给定这些输入,我会产生这些输出并保持这些规则。”当承诺清晰时,团队就能并行工作:
这不是官僚主义——而是在不断增长的代码库里创建安全的协调点。
你不需要宏大的架构审查来改进模块化。试这些轻量检查:
划定良好的边界会把“变更”从系统级事件变成局部编辑。
当软件小的时候,你可以“把一切记在脑中”。到了规模化,这种做法不再成立——失败模式也变得熟悉。
常见症状包括:
迪杰斯特拉的核心押注是:人是瓶颈。清晰的控制流、小而明确的单元以及你能推理的代码不是审美选择——它们是提升产能的手段。
在大型代码库中,结构相当于对理解的压缩。如果函数有明确的输入/输出,模块有可以命名的边界,“顺利路径”没有与每个边界情况纠缠在一起,开发者就会花更少时间重建意图,更多时间去做有意图的改动。
随着团队增长,沟通成本比代码行数增长得更快。有纪律、可读的代码减少了参与所需的部落知识。
这会立即体现在入职上:新工程师能遵循可预测模式,学会一小组约定,并在无需长时间了解“陷阱”的情况下做出改动。代码本身会教会系统。
发生事件时,时间稀缺而信心脆弱。用明确假设(前置条件)、有意义检查和直线控制流写的代码在压力下更容易追踪。
同样重要的是,有纪律的改动更容易回滚。小而局部的编辑和清晰的边界减少了回滚触发新故障的概率。结果不是完美——而是更少惊讶、更快恢复,以及系统在若干年和众多贡献者积累后仍可维护。
迪杰斯特拉的观点不是“用旧方法写代码”。而是“写你能解释的代码”。你可以在不把每个特性变成形式证明练习的情况下采纳这种心态。
先从使推理便宜的选择开始:
一个好启发式:如果你无法用一句话概括函数保证了什么,它很可能做得太多。
你不需要一次大重构。沿着接口缝隙逐步增加结构:
isEligibleForRefund)。这些升级是增量的:它们为下一次改动降低认知负担。
在审查(或撰写)改动时,问自己:
如果审查者无法迅速回答,代码就在示意隐藏的依赖。
重复代码的注释会过时。相反,写明为什么代码是正确的:关键假设、你防护的边界情况以及如果假设改变会有什么被破坏。短短一句“不变式:total 始终等于已处理项之和”可能比一段解释性文字更有价值。
如果你想把这些习惯轻量化地收集起来,可以把它们做成一份共享清单(参见 /blog/practical-checklist-for-disciplined-code)。
现代团队越来越多地使用 AI 加速交付。风险是熟悉的:今天的速度如果产生的代码难以解释,明天就会变成混乱。
一种符合迪杰斯特拉精神的使用 AI 的方式是把它视为促进结构化思考的加速器,而不是替代品。例如在 Koder.ai 中构建时——一个通过聊天创建 Web、后端与移动应用的 vibe-coding 平台——你可以通过明确提示与审查步骤来保持“先推理”的习惯:
即便你最终把源代码导出并在别处运行,同样的原则适用:生成的代码应该是你能解释的代码。
这是一个轻量的“迪杰斯特拉友好”清单,可在审查、重构或合并前使用。它不是让你整天写证明——而是把正确性与清晰设为默认值。
total 始终等于已处理项之和”也能防止微妙的 bug。挑一个混乱的模块,先重构控制流:
然后在新边界周围添加一些针对性的测试。如果你想看更多类似模式的文章,请浏览 /blog。
因为当代码库变大时,瓶颈不是敲代码的速度,而是理解的能力。迪杰斯特拉强调可预测的控制流、清晰的契约和正确性,这些能减少“一个小改动会在别处造成惊喜”的风险——而这种风险正是随着时间让团队变慢的根源。
在本文里,“规模”并不是主要指性能,而是指复杂性如何成倍增长:
这些因素让可推理性和可预测性比“巧妙”更有价值。
结构化编程偏好一小组清晰的控制结构:
if/else、switch)for、while)目标不是僵化,而是让执行路径容易跟踪,从而能解释行为、审查改动并调试时不用“在文件间瞬移”。
问题在于不受约束的跳转会产生难以预测的执行路径和不清晰的状态。当控制流纠缠在一起时,开发者会花很多时间回答“我们怎么到这里的?”或“这个变量现在是什么状态?”现代的等价问题包括深度嵌套的分支、分散的早期返回和隐含的状态更改,这些都会让行为难以追踪。
正确性是用户依赖的“静默特性”:系统始终按承诺工作,并在无法满足时以可预测、可解释的方式失败。它区分了“在几个示例下有效”和“在重构、集成与边界情况出现后仍然可靠”。
因为依赖会放大错误。一个小的错误状态或边界问题会被复制、缓存、重试、被其他模块包裹或用临时补丁绕过。久而久之,团队不再问“什么是真的?”,而是依赖“通常会发生什么”,这会让事件调查和修复变得昂贵且危险。
这里的简单性是指同时运行的想法少:职责清晰、数据流清楚、尽量少的特殊情况。它不是追求最少行数或炫技的一行写法。
检验标准是:当需求变化时,行为是否仍然容易预测。如果每个新情况都引入“除非……”“例外当……”,你就在累积偶然复杂度。
不变式是不论循环或状态转换中某些事实始终成立的断言。轻量用法示例:
total 等于已处理项的和)。这样后续改动时,下一位开发者知道哪些事实必须被保留,从而更安全地修改代码。
测试通过运行示例发现问题;推理则通过使逻辑明确来防止整类问题。测试无法证明不存在缺陷,因为它无法覆盖所有输入、时序或特性组合。推理在代价高昂的故障领域(资金、并发、安全)尤其有用。
实用的混合方式是:广泛测试 + 针对性断言 + 在关键逻辑周围明确前置/后置条件。
从一些可重复的小动作开始,降低认知负担:
这些是渐进的“结构升级”,能在不做大规模重构的情况下,让后续改动更便宜、更安全。