软件工程定律:50 年踩坑智慧的结晶
每隔一段时间,总有人在项目里重新发现同一个道理:往进度已经落后的项目里加人,只会让项目更晚交付。这个判断不是凭感觉,它有名字,叫 Brooks’s Law,1975 年就写进了《人月神话》。
问题在于,这类教训极难传承。前人的血泪藏在绝版书里、散落在博客角落,或者以"老鸟的直觉"的形式存在于某位资深工程师的脑子里。每一代工程师都得重新踩一遍。
Dr. Milan Milanović 花了几年时间,把这些分散在 50 年历史中的规律收集整理,做成了 Laws of Software Engineering 网站和同名书籍,收录了 56 条核心定律,加上伴随原则总计 63+ 条定律与原则。这篇文章不是书的摘要,而是试图从工程师的角度,把其中最值得深思的几条拉出来,讲清楚它们背后的逻辑。
一张地图:七个维度的定律体系
这 63+ 条定律大致覆盖七个领域。把它们的关系画出来,会发现一件有趣的事:不同维度的定律之间经常互相印证,甚至互相矛盾。这种张力,正是软件工程的本质。
mindmap
root((软件工程定律))
架构与复杂度
Gall's Law
Law of Leaky Abstractions
Tesler's Law
CAP Theorem
Hyrum's Law
人员与组织
Conway's Law
Brooks's Law
Dunbar's Number
Bus Factor
时间与估算
Hofstadter's Law
Parkinson's Law
90-90 Rule
Goodhart's Law
质量与演化
Murphy's Law
Boy Scout Rule
Technical Debt
Lehman's Laws
规模与性能
Amdahl's Law
Metcalfe's Law
编码与设计
DRY / KISS / YAGNI
SOLID
Law of Demeter
决策与认知偏差
Dunning-Kruger Effect
Sunk Cost Fallacy
Confirmation Bias
Part I:架构与复杂度
Gall’s Law:从简单开始,别想着一步到位
“A complex system that works is invariably found to have evolved from a simple system that worked. A complex system built from scratch won’t work.”
这条定律出自 John Gall 1975 年的著作《Systemantics》——一本对系统理论的批判性审视,但它在软件领域几乎无处不在。每次大型重写项目的失败,背后几乎都有它的影子。
原因并不神秘:我们对复杂系统的理解,往往只能通过运行它才能获得。一个设计精良但从未投产的系统,必然遗漏了大量只有在生产环境里才能发现的边界条件、状态转换和外部依赖。
Google 的很多核心基础设施、Linux 内核,都是从极简的原型演化来的。反面案例更多,但大多数人讳莫如深。
实践含义很直接:新功能从最简单可用的版本开始,验证假设之后再迭代复杂度。 不是因为工程师偷懒,而是因为这是唯一经过验证有效的路径。
Law of Leaky Abstractions:抽象总是有漏洞的
Joel Spolsky 在 2002 年提出这条定律:
“All non-trivial abstractions, to some degree, are leaky.”
抽象的作用是屏蔽复杂性。TCP/IP 让你不必关心数据包丢失;ORM 让你不必手写 SQL;云存储让你不必管磁盘故障。但当问题出现时,你还是得理解被屏蔽的那一层。
用 ORM 遇到了 N+1 查询问题,你得懂 SQL。用 TCP 做实时通信出现延迟抖动,你得懂网络层。这不是 ORM 或 TCP 设计得差,而是抽象的本质决定的。
这条定律的实用结论不是"别用抽象",而是"了解你依赖的抽象层的内部机制"。 知道什么时候抽象会失效,比永远不用抽象要务实得多。
Tesler’s Law:复杂度不会消失,只会转移
Larry Tesler 的守恒定律:
“Every application has an inherent amount of irreducible complexity. The only question is who will have to deal with it.”
这是一个零和游戏。简化用户界面,意味着复杂度被推到了开发者身上。简化 API,意味着复杂度被推到了实现层。简化配置,意味着复杂度被推到了约定和文档里。
这不是悲观的结论,而是一个资源分配问题:把复杂度推给最有能力处理它的地方。一般来说,这意味着更多的复杂度应该在系统内部,而不是暴露给终端用户。
Hyrum’s Law:足够多的用户会依赖你的 bug
这是 Google 工程师 Hyrum Wright 在大规模 API 维护实践中总结出的:
“With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.”
换句话说,你的接口文档声明的是契约,但用户依赖的是行为。 当用户足够多时,任何可观察的行为,包括 bug、响应时间的规律、错误消息的格式,都会被某个用户所依赖。
这条定律在 HackerNews 的讨论中被反复提及,因为它解释了为什么大规模系统的向下兼容如此痛苦:你想修复的那个 bug,可能正是某个用户的核心功能。Google 内部的 API 变更实践,很大程度上是在和这条定律正面交锋。
Part II:人员与组织
这一部分的定律往往比技术定律更让人感到不舒服,因为它们在说的是我们自己。
Conway’s Law:组织结构决定系统结构
Melvin Conway 在 1967 年写道:
“Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.”
这条定律有大量实证支持。MIT 和哈佛商学院的研究者发现,分布式团队开发出的软件,在模块耦合度上与集中式团队有显著差异。
实际案例触手可及:微服务架构兴起之前,很多公司就已经按业务线划分了团队,他们的单体应用内部实际上已经呈现出了服务化的边界。微服务架构的流行,某种程度上是 Conway’s Law 的一次显性化。
逆用这条定律,就是"逆康威操控"(Inverse Conway Maneuver):先设计目标架构,再调整团队结构来适配它。Spotify 的 Squad 模型、Amazon 的两披萨团队原则,都是这种思路的实践。
Brooks’s Law:加人不解决问题,往往制造更多问题
Fred Brooks 在 1975 年的《人月神话》中提出:
“Adding manpower to a late software project makes it later.”
原因有两个:首先,新人需要时间上手(ramp-up),在这段时间里他们不仅没有产出,还消耗了老员工的时间进行知识传递。其次,团队规模增加,沟通成本呈指数级上升。n 个人之间有 n*(n-1)/2 条沟通路径。
这不是说永远不该扩充团队,而是说时机很关键:在项目初期、设计阶段扩充资源,效果远好于在交付截止日之前突击加人。
Brooks’s Law 经常和 Dunbar’s Number 一起理解:人类能维持稳定社会关系的上限大约是 150 人。超过这个规模的组织,需要刻意设计正式的沟通机制来补偿自然社会关系的失效。
graph LR
A[项目延期] -->|直觉反应| B[增加人手]
B --> C[新成员上手期]
B --> D[沟通成本指数增加]
C --> E[项目更延期]
D --> E
Bus Factor:知识集中是一种隐藏的技术债
Bus Factor(公交车系数)定义为:最少需要有几个关键成员"被公交车撞了"(离职/病假/休假),项目就会陷入严重困境。
一个 Bus Factor 为 1 的项目,意味着有某个人掌握着其他人都不知道的关键知识或权限。这在早期初创公司极为常见,往往在那个"不可替代"的人离职后才被发现问题的严重性。
提升 Bus Factor 的方法:代码评审(让至少两个人了解每个模块)、文档、知识分享会、轮岗。这些实践通常被认为是"开销",但从 Bus Factor 的角度来看,它们是降低组织风险的直接投资。
Part III:时间与估算
Hofstadter’s Law:自我指涉的估算悖论
Douglas Hofstadter 在《哥德尔、艾舍尔、巴赫》中写道:
“It always takes longer than you expect, even when you take into account Hofstadter’s Law.”
这条定律的结构本身就是它内容的最好注解——它是递归的,用来描述一个递归的问题:即使你知道自己会低估,你还是会低估。
认知科学上有一个解释:规划谬误(Planning Fallacy)。人类在估算未来任务时,倾向于以最理想的情形为基准,忽略过去类似任务实际花费的时间,也低估了未知风险的可能性。
软件项目的估算天然受这个偏差影响,叠加上 Ninety-Ninety Rule(Tom Cargill 归纳于 Bell Labs:前 90% 的代码用了 90% 的时间,剩下 10% 的代码用了另外 90% 的时间),结果几乎注定是延期。
比较有效的对抗策略不是"更仔细地估算",而是结构性地引入参照类(reference class forecasting):查看过去类似项目的实际完成时间,以此为基准做调整,而非凭空推算。
Parkinson’s Law:工作膨胀以填满时间
“Work expands so as to fill the time available for its completion.”
Cyril Northcote Parkinson 最初是观察英国官僚机构写出这个规律的,但它在软件团队里同样适用。
给一个任务两天,它就需要两天;给一周,它就需要一周。这不完全是因为工程师懒散,而是因为在有剩余时间时,人会自然地追求完美、增加功能、重构代码——即使这些改动并不在原始需求里。
这条定律和 YAGNI(You Aren’t Gonna Need It)有强烈的共鸣:时间约束有时是生产力的朋友。适度的 deadline 压力能帮助团队区分"必要"和"想要"。
Goodhart’s Law:指标成为目标后就失效了
这条来自经济学的定律在软件工程中被反复验证:
“When a measure becomes a target, it ceases to be a good measure.”
代码覆盖率是一个典型案例。为了达到 80% 的覆盖率目标,工程师可能写出大量无实际价值的测试——覆盖了代码行,但没有验证业务逻辑。结果是数字好看,质量原地踏步。
同样的问题出现在 velocity(速度点数)、bug 数量、PR 合并次数等几乎所有软件工程指标上。指标应该是观察窗口,而不是优化目标。当管理者开始用指标考核时,这个指标就开始失真了。
Part IV:质量、维护与演化
Lehman’s Laws:软件的熵增是不可逆的
Meir Lehman 在 1970-1980 年代通过研究大型软件系统,总结出了一系列关于软件演化的规律,其中最核心的两条是:
- 持续变化定律:与真实世界交互的系统必须持续演化,否则就会变得越来越不满足需求。
- 复杂度增长定律:随着系统演化,其复杂度会持续增加,除非有明确的投入来降低它。
这两条定律放在一起,描述了软件维护的根本困境:你必须改变系统,但每次改变都会增加复杂度。不改,系统腐烂;改,系统变复杂。唯一的出路是持续的重构投入,把复杂度的增长速度控制在可接受范围内。
这也是技术债(Technical Debt)概念的理论基础:它不是"以后再还"的可选项,而是系统演化的自然代价,需要被纳入日常开发的预算里。
The Boy Scout Rule:让营地比你来时更干净
Robert C. Martin(Uncle Bob)借用了童子军格言:
“Always leave the code cleaner than you found it.”
这是一个对抗代码腐烂(code rot)的实践原则。如果每个进入某段代码的工程师,都顺手改善一点命名、消除一个重复、补充一条注释,代码库的整体质量就会随着时间缓慢上升,而不是不可避免地下沉。
关键是"顺手"——不是专门开票重构,而是在完成主要任务时附带做一点改进。这降低了每次改进的成本,也避免了"大规模重构"那种高风险、长周期的做法。
Kernighan’s Law:调试比编写困难两倍
Brian Kernighan 写道:
“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.”
这条定律有很强的实践含义:代码的首要受众是人,其次才是机器。炫技的代码节省了编写时间,却成倍增加了后续维护的成本——包括调试、代码审查和功能迭代的成本。
可读性不是奢侈品,是工程实践的基础设施。
Part V:编码与设计原则
这一类更像是可操作的工程原则,而不是描述性的规律,但它们在实践中的指导价值同样巨大。
DRY、KISS 和 YAGNI:三位一体的简洁哲学
- DRY(Don’t Repeat Yourself):同一知识在系统中只应有一个权威来源。重复的代码意味着重复的维护成本,更危险的是,修改时漏掉了某个副本。
- KISS(Keep It Simple, Stupid):能用简单方案解决的,不要引入复杂方案。复杂度有隐藏成本:学习曲线、调试难度、演化阻力。
- YAGNI(You Aren’t Gonna Need It):不要实现当前不需要的功能。过度的"前瞻性设计"往往是在为一个永远不会到来的未来付出现实的代价。
三者之间有微妙的张力:DRY 有时会引入不必要的抽象(违反 KISS);KISS 有时意味着接受一定程度的重复(违反 DRY)。判断何时遵循哪条原则,需要具体场景的工程判断,没有放之四海皆准的答案。
Law of Demeter:最少知识原则
这条 1987 年提出的原则说:一个对象应该只与它的直接邻居交互,而不应该通过链式调用深入到邻居的内部。
| |
这不仅是代码风格的问题,而是耦合度的问题。链式调用意味着当前代码对整个调用链上的每个中间对象的内部结构都产生了依赖。任何一处改变都可能波及到这里。
Part VI:认知与决策偏差
这一部分是很多工程师最容易忽视的,因为它不直接涉及技术问题,而是涉及我们自己的思维方式。
Dunning-Kruger Effect:你不知道你不知道什么
这个认知心理学研究发现:能力不足的人,往往低估了自己能力的不足,因为识别技能差距本身需要一定水平的元认知能力。
在软件工程里,这体现在:初级工程师往往对项目复杂度估计不足,对自己的设计过度自信;反而是经验丰富的工程师,知道自己的知识边界在哪里,会更谨慎地评估风险。
有意思的是,这条定律的反面是冒名顶替综合征(Impostor Syndrome):真正能力强的人,反而容易低估自己,觉得自己不够格。两者常常在同一个团队里同时存在。
Sunk Cost Fallacy:沉没成本不是继续的理由
“我们已经在这个方案上投入了六个月,现在不能放弃。”
这是软件项目中最常见的决策谬误之一。沉没成本(已经花出去的时间和资源)不应该影响未来决策,因为无论做什么选择,那些成本都回不来了。
继续一个失败方向的真实代价,是继续投入的时间和资源,而不是之前已经花掉的那些。决策应该基于未来的预期收益和成本,而不是对过去投入的心理依附。
放弃一个走了六个月的错误路径,需要很大的勇气,但通常比继续走下去代价更低。
定律之间的矛盾:这才是真正的挑战
这些定律最有意思的地方不是每一条单独看,而是它们放在一起时的张力:
- DRY 与 YAGNI 的矛盾:为了消除重复引入抽象,但那个抽象可能是你不需要的。
- Gall’s Law 与 Second-System Effect 的矛盾:从简单开始演化,但当你要重写成熟系统时,又容易过度设计。
- Knuth’s Optimization Principle 与现代性能要求的矛盾:HackerNews 讨论中有人指出,在 1974 年,优化意味着写汇编;在今天,架构层面的性能决策必须在一开始就做,否则后期根本无法修改。
这些矛盾没有统一答案。定律给你的是框架,不是算法。知道这些规律,是为了在具体场景下做出更有意识的权衡,而不是为了找到一个万能的规则并死守它。
给工程师的实用地图
flowchart TD
A[遇到工程决策] --> B{什么类型的问题?}
B -->|项目延期/进度压力| C["Brooks's Law\nHofstadter's Law\n90-90 Rule"]
B -->|架构设计选择| D["Gall's Law\nConway's Law\nYAGNI/KISS"]
B -->|代码质量/维护| E["Boy Scout Rule\nKernighan's Law\nDRY/Law of Demeter"]
B -->|团队与组织| F["Dunbar's Number\nBus Factor\nConway's Law"]
B -->|指标与度量| G["Goodhart's Law\nGilb's Law"]
B -->|技术决策倾向于放弃| H["Sunk Cost Fallacy\nConfirmation Bias"]
结语
这些定律存在的价值,不在于给你提供一套可以机械执行的规则,而在于给你一个词汇表——用来描述你已经见过、但可能还没有命名的现象。
当你下次看到一个团队因为引入了太多新成员导致沟通成本激增时,你知道那是 Brooks’s Law 在起作用。当你看到一个 API 的某个 bug 被用户当成特性使用,不敢轻易修复时,你知道那是 Hyrum’s Law 的经典案例。
有了名字,才能更清晰地讨论,才能更有意识地决策。
这些规律从 60 年代开始积累,有些有研究支持,有些只是经验归纳。它们不是物理定律,有反例,有边界条件。最糟糕的用法,是把它们当成不容置疑的权威来压制异议;最好的用法,是把它们当成思考的起点。
参考资料
- Laws of Software Engineering — Dr. Milan Milanović 整理的定律合集与配套书籍
- Laws of Software Engineering: Hacker News Discussion — 工程师社区对这些定律的讨论,包含大量来自一线实践的补充观点
- The Mythical Man-Month — Fred Brooks,1975
- How Do Committees Invent? — Melvin Conway,1968
- The Law of Leaky Abstractions — Joel Spolsky,2002
- Structured Design — Lehman’s Laws 原始论文