乐于分享
好东西不私藏

软件工程的 50 条经验定律(下):设计、估算与判断

软件工程的 50 条经验定律(下):设计、估算与判断

软件工程的 50 条经验定律(下):设计、估算与判断

本文是《软件工程的 50 条经验定律》系列的第三篇,也是终篇。原始素材来自 Laws of Software Engineering[1]

上中下三篇:

  • 上篇 · 人与组织(团队、晋升、个体偏差)
  • 中篇 · 架构与系统(接口、演化、分布式、规模)
  • 下篇 · 设计、估算与判断(本篇)

序:日常工作的定律密度最高

上篇 · 人与组织讲的是谁在写代码,中篇 · 架构与系统讲的是代码在哪里跑。这一篇讲的是今天上午 10 点你坐在工位前、正要写一行代码、改一个 PR、估一个工期、做一个选型的时刻。

最常用的定律密度,全在这里。22 条,按”设计原则 → 质量与维护 → 估算与计划 → 决策与思维”四组排开。


一、设计原则(6 条)

1. YAGNI — You Aren’t Gonna Need It

Don’t add functionality until it is necessary.

写一个函数顺手加五个”以后可能用得到”的参数,三年后这函数还是只有一个调用点。未来的你比现在的你聪明,把决定权留给未来的你。

想加”扩展点”或”可配置项”时,先问:现在有第二个用法吗?没有就不加,等真有了第二个再重构。

2. DRY — Don’t Repeat Yourself

Every piece of knowledge must have a single, unambiguous, authoritative representation.

注意 DRY 的本意是”知识不重复”,不是”代码不重复”。两段长得一样但属于不同业务的代码,是巧合不是重复——强行 DRY 会绑死两个本应独立演化的东西。

先重复,再 DRY。过早抽象比重复更危险。Rule of Three:同样的模式出现三次再考虑提取。

3. KISS — Keep It Simple, Stupid

Designs and systems should be as simple as possible.

能简单就简单,不要因为”显得专业”加复杂度。100 行 Python 能解决的事没必要拉一个微服务集群;一个 if-else 能解决的事没必要上策略模式 + 工厂方法。简单的方案不是不专业,是更专业。

评审方案时问一句:这个设计的最简单版本是什么?为什么不用那个?

4. SOLID Principles

Five main guidelines that enhance software design.

五个面向对象设计原则——SRP(单一职责)、OCP(开闭原则)、LSP(里氏替换)、ISP(接口隔离)、DIP(依赖倒置)。

SOLID 是教科书的事,但日常最常违反的是 SRP——一个类同时管业务规则、数据库读写、HTTP 序列化,一改动一个连锁三处。SRP 是 SOLID 里 ROI 最高的一条,其他几条不必死守,单一职责值得反复贯彻。

5. Law of Demeter(迪米特法则)

An object should only interact with its immediate friends, not strangers.

一个对象只跟自己的直接邻居说话,不要绕过邻居去找邻居的邻居。order.getCustomer().getAddress().getCity().getName() 这种链式调用就是典型违反——一旦中间任何一层结构变了,这串代码就崩。

发现链式调用三层以上,考虑给最外层加一个直接方法(如 order.getCustomerCity())。把”导航”封装在内部,外面只看接口。

6. Principle of Least Astonishment(最少惊讶原则)

Software and interfaces should behave in a way that least surprises users and other developers.

函数叫 delete() 但其实是软删除;变量叫 is_active 但返回的不是布尔值;函数有副作用但名字没暗示。每次”啊?”都是一次债务。

命名 = 承诺。你给函数取的名字,就是和未来读这段代码的人签的合同。


二、质量与维护(9 条)

7. Boy Scout Rule(童子军法则)

Leave the code better than you found it.

每个 PR 顺手清理一两个 magic number、加一行注释、删一段死代码——一年累计是巨大的代码质量改善,且摊销在所有人身上。

但要有度,别在 bug 修复 PR 里夹带架构重构——那是另一种债务。

8. Broken Windows Theory(破窗效应)

Don’t leave broken windows (bad designs, wrong decisions, or poor code) unrepaired.

一扇没修的破窗会很快带来更多破窗。代码也一样:项目里出现一处 // TODO: 临时方案,下次重构掉 而没人管,半年后整个目录都是这种 TODO。低质量是有传染性的——人们看到周围都是垃圾,就觉得自己也可以扔垃圾。

第一扇破窗很贵,十扇破窗后就免疫了。要么修,要么删 TODO,不要留下”已知不修”的烂摊子。

9. Technical Debt(技术债)

Technical Debt is everything that slows us down when developing software.

技术债不只是烂代码,它包括缺测试、缺文档、过期依赖、未自动化的部署、命名混乱、目录组织失控。衡量标准是”它现在让我多花了多少时间”。

技术债不是要清零,是要管理,像财务债一样——有计划地承担、有计划地偿还。每个 sprint 留 10-20% 给”还债”,比每年一次大重构有效得多。

10. Kernighan’s Law(柯尼汉定律)

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.

调试比写代码难一倍。如果你已经把写代码的脑力全用完了,调试时就没脑力剩了。你刚写完一段炫技代码跑通了,过两个月再看——根本看不懂自己写了什么,更别说改了。

写代码时主动留余地。别用最炫的语法,别用最聪明的算法(除非真的需要),别在一行里塞三个三元表达式。把”未来调试它的人”当成有 60 分智力的自己。

11. Testing Pyramid(测试金字塔)

A project should have many fast unit tests, fewer integration tests, and only a small number of UI tests.

单元测试要多且快,集成测试少一点,UI 端到端测试再少一点。如果你的测试矩阵是倒金字塔(大量 UI 测试、少量单测),CI 会越来越慢、越来越脆——UI 测试容易因为跟业务无关的小变化挂掉。

新增一个端到端测试前先问:这个能不能用单测覆盖?大部分时候答案是能。

12. Pesticide Paradox(杀虫剂悖论)

Repeatedly running the same tests becomes less effective over time.

同样的测试反复跑,效果会逐渐递减。你写了一百个单测,全绿,但 bug 还是在生产环境冒出来——因为 bug 不会出现在你已经测过的地方。

定期更新测试用例,结合 fuzzing、property-based testing、混沌工程引入”未预期的输入”。绿色不等于安全,只等于已知问题没回归。

13. Sturgeon’s Law(斯特金定律)

90% of everything is crap.

任何领域 90% 的东西都是垃圾。90% 的开源项目没人用,90% 的论文没人引,90% 的”AI 工具”明年就消失。这不是悲观,是数学——长尾分布的常态。

技术选型时降低对”全网评价”的权重——评论区都在夸的工具,可能在那 90% 里。看真实用户、看长期维护、看 GitHub 提交记录的活跃度。

14. Murphy’s Law(墨菲定律)

Anything that can go wrong will go wrong.

会出错的事,迟早会出错。你以为这个分支永远走不到——上线第三天就走到了。你以为这个错误永远不会发生——它在客户演示当天发生了。

把”不可能”当成”低概率”对待。关键路径上永远准备 fallback、超时、降级,不是因为你悲观,是因为你尊重墨菲。

15. Premature Optimization(过早优化)

Premature optimization is the root of all evil.

过早优化是万恶之源(Knuth 的原话)。还没写完功能、没跑过 benchmark、不知道瓶颈在哪,就开始用 SIMD、写汇编、调缓存对齐——结果代码复杂度爆炸,性能没提升。

先正确,再清晰,最后才优化。要优化时先 profile,找到真正的瓶颈再下刀。Knuth 的原话还有后半句:”但我们不应该错过那关键的 3% 优化机会”——意思是优化要做,但要做对地方。


三、估算与计划(5 条)

16. Hofstadter’s Law(侯世达定律)

It always takes longer than you expect, even when you take into account Hofstadter’s Law.

任何事都比你预期的久——即使你已经把这条定律考虑在内。这是软件工程最优雅的递归笑话,也是最准的估算定律。

估完工时把数字乘 2,仍然偏乐观。更靠谱的方式是承诺范围而不是时间——”两周内交付登录 + 列表页”比”两周内做完 X”靠谱得多。

17. The Ninety-Ninety Rule(90-90 法则)

The first 90% of the code accounts for the first 90% of development time. The remaining 10% accounts for the other 90%.

代码的前 90% 占了前 90% 的工期,剩下的 10% 占了另外 90% 的工期。听起来像段子,但你做过任何”差最后一点就能上线”的项目就知道这是写实主义——那 10% 通常是边界情况、性能调优、环境兼容,它们不在初始 estimation 里。

当你说”还差最后一点”时,提醒自己:这一点可能就是另一半工时。提前留出灰度 + 修 bug 的窗口。

18. Parkinson’s Law(帕金森定律)

Work expands to fill the time available for its completion.

工作总会膨胀到填满你给它的时间。给一个任务一周,你会用一周;给同样的任务三天,你也能交付。截止时间不是预测,是约束。

主动用紧凑的时间盒约束自己——番茄工作法、sprint、timebox 都是利用 Parkinson 的反向操作。但别滥用,长期用紧时间会把人榨干。

19. Goodhart’s Law(古德哈特定律)

When a measure becomes a target, it ceases to be a good measure.

当一个测量值变成目标,它就不再是好的测量值。把”代码行数”当绩效,工程师立刻写更长的代码;把”P95 延迟 < 100ms”作为唯一指标,团队会优化掉一些功能来达标。

技术指标本身没错,但任何 KPI 都会被博弈。永远准备一组反向指标——盯延迟时也盯可读性,盯增长时也盯留存。

20. Gilb’s Law(吉尔布定律)

Anything you need to quantify can be measured in some way better than not measuring it.

任何你需要量化的东西,都有某种方法可以测量——而且总比不测量好。

常听到”用户体验没法量化”——其实可以:留存率、点击深度、NPS、表单完成率。”代码质量没法量化”——也可以:圈复杂度、改动后 bug 率、PR 的 review 轮数。没完美指标,但坏指标好过没指标。


四、决策与思维(7 条)

21. Occam’s Razor(奥卡姆剃刀)

The simplest explanation is often the most accurate one.

在多种解释中,假设最少的那个通常是对的。线上出 bug,可能性 A:”是宇宙射线导致的位翻转”;可能性 B:”我昨天改的那行代码有问题”——先怀疑 B。

调试和决策时,优先验证最简单的假设。复杂解释要等简单解释被排除后再考虑。

22. The Map Is Not the Territory(地图不是疆域)

Our representations of reality are not the same as reality itself.

你画的图、写的文档、做的模型,都不是现实本身。架构图上的方块和实际跑起来的服务行为是两件事;监控仪表盘上的指标和真实用户体验是两件事。

地图很有用,但别把地图当成疆域。定期下一线——读真实日志、看真实用户、跑真实场景。文档和图表是工具,不是真理。

23. The Hype Cycle & Amara’s Law(炒作周期 / 阿马拉定律)

We tend to overestimate the effect of a technology in the short run and underestimate the impact in the long run.

人们短期高估一项技术,长期低估它。区块链 2017 高估了,但 2024 它在某些场景静悄悄地在用;AI 2023 被高估,但 2030 之后的影响可能比 2023 的炒作还大。

技术决策时穿过炒作周期看本质。别在 hype 顶点 all-in,也别在幻灭低谷彻底放弃。

24. First Principles Thinking(第一性原理)

Breaking a complex problem into its most basic blocks and then building up from there.

把问题拆到最基础的真元素,然后从这些元素重新搭起来。SpaceX 之所以能把火箭成本降一个数量级,是因为他们没问”火箭历史上为什么那么贵”,而是问”火箭由什么组成、每个原料按市场价是多少钱”。

当你听到”行业惯例就是这样”时,问一句:为什么?拆到底层原因,然后从底层重新推一遍。很多”行业惯例”经不起这个推法。

25. Inversion(反向思考)

Solving a problem by considering the opposite outcome and working backward from it.

与其问”怎么成功”,先问”怎么失败”,然后避开。”怎么做出一个用户喜欢的产品”很难,但”怎么做出一个用户讨厌的产品”很容易——加广告、做反人类的弹窗、复杂的注销流程。避开这些就等于正向解决了一半。

项目复盘前问一句:我们怎么样才能让这个项目失败?列出来,然后看自己有没有在做这些事。

26. Pareto Principle(80/20 原则)

80% of the problems result from 20% of the causes.

80% 的问题来自 20% 的原因。80% 的 bug 集中在 20% 的代码里;80% 的客诉来自 20% 的功能;80% 的性能问题来自 20% 的查询。

不要平均用力。找到那 20%——读 git blame 里的”高频改动文件”,看 monitoring 里 top 5 的慢查询,分析客诉里的高频关键词。集中火力打那 20%,比全面优化高效得多。

27. Cunningham’s Law(坎宁安定律)

The best way to get the correct answer on the Internet is not to ask a question, it’s to post the wrong answer.

人对”指出别人的错”远比对”回答提问”更有动力。

团队讨论卡住时,先抛一个不完美的方案——人们会立刻挑刺、补充。RFC 文化的底层逻辑就是这个:先有具体提议再批判,比围绕一张白纸高效一百倍。


写在系列收尾

回头看这三篇 50+ 条定律,会发现它们其实在描述同一件事:

软件工程是一门”和默认结果对抗”的工艺。

如果你不主动做事,默认结果是:系统会镜像组织(Conway),估算会过于乐观(Hofstadter),复杂度会持续累积(Lehman),抽象会漏(漏抽象),KPI 会被博弈(Goodhart),接口的偶然行为会被依赖(Hyrum),沉没成本会让你陷在错的方向,测试会逐渐失效(杀虫剂),90% 的东西是垃圾(Sturgeon)。

这些”默认结果”不是因为人懒或者笨,是因为它们是系统的自然倾向。工程师真正的工作,是识别这些自然倾向,并在它们伤到自己之前主动干预。

定律本身没法替你做决定,但它们能帮你在岔路口想起来——这个坑前面有人埋过,路标在那里。


全系列

  • 上篇 · 人与组织[10]
  • 中篇 · 架构与系统[11]
  • 下篇 · 设计、估算与判断(本篇)

引用链接

[1]Laws of Software Engineering: https://lawsofsoftwareengineering.com/