读博客:The Law of Leaky Abstractions

2026-09-023 出链1 引用

所有非平凡的抽象都会在某种程度上泄漏:抽象省工作时间但不省学习时间,能力来自能穿透抽象、理解底层。

https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions

  • 是什么(What)
    • 抽象 = 对底层复杂事物的简化,是"假装"底层不存在:TCP 假装网络可靠、文件系统假装硬盘只是文件夹层级、string 库假装字符串和数字一样容易。
    • 定律:All non-trivial abstractions, to some degree, are leaky. 抽象只能保护它承诺覆盖的失败模式,超出设计假设的底层现实会穿透出来。
  • 为什么(Why)
    • 底层复杂性不会因为被隐藏而消失,只是被推迟、被转移;"像 X 一样容易"以"X 的硬约束仍在底下"为代价,场景越出假设时现实必然暴露。
    • 学习成本守恒:抽象节省工作时间,但不节省学习时间;抽象层次堆得越高,越要把每一层之下的东西都学会。
  • 怎么做(How)
    • 学习:对每个抽象先问"它建立在什么之上?什么条件会击穿它?";先学会手动做事,再依赖工具提效。
    • 排障:高层行为异常时先假设泄漏,沿协议栈向下检查,不要在抽象层反复找不可能的解释。
    • 设计:契约小而诚实,只承诺能兑现的;把已知泄漏点写进文档而不是藏起来。
    • 边界:性能与正确性无法在抽象层解释时,就穿透它——穿透能力是长期执照。

抽象并没有消灭复杂度,它只是把复杂度推迟到“异常、规模、性能、故障”出现的时候。

核心观点:

所有非平凡的抽象,在某种程度上都是泄漏的。抽象替我们省下了工作,却没有替我们省下学习。

例证一:TCP——建在不可靠之上的"可靠"

TCP 是互联网日常依赖的魔法:它建立在不可靠的 IP 之上,却承诺"消息一定到达、不会损坏"。但这是简化过的说法:

  • 宠物蛇咬断网线:没有 IP 包能通过,TCP 无能为力,消息就是不达;
  • 被接入过载的 hub:部分包能过,TCP 照常工作,但一切慢得像爬。

一旦发生,你就切身碰到了"这个抽象保护不了你什么"的边界——这就是泄漏(leak)。

例证二:泄漏无处不在

  • SQL:声明式查询抽象掉过程,只表达"要什么"。但逻辑等价的查询性能可差千倍——有些 SQL 服务器在 where a=b and b=c 上极慢,补上 and a=c(结果集完全相同)却快得多。抽象泄漏后,你不得不打开查询计划分析器研究它做错了什么。
  • NFS / SMB:远程文件"像本地文件一样",直到连接变慢或断开。具体事故:用户主目录在 NFS 上(抽象一)、用户用 .forward 转发邮件(抽象二)、NFS 服务器在收信时宕机——.forward 找不到,转发静默失败,几封邮件直接丢失。每一层都"负责一点",叠加起来却没有一层负责端到端语义。
  • C++ string:试图让你假装字符串和整数一样是一等公民,几乎所有 string 类都重载 + 以支持 s + "bar"——但没有任何类能让你写 "foo" + "bar",因为字符串字面量永远是 char*。这个语言层面堵不上的漏洞整段泄漏,C++ 的历史本身就是一部修补 string 抽象漏洞的历史。
  • ASP.NET:抽象掉 hyperlink 与 button 的区别,让"拖控件 + 写服务端代码"成为全部。但 HTML 里超链接没法提交表单,框架用隐藏 JavaScript 的 onclick 补上——用户禁用 JavaScript 时整个应用失效,而不懂它在抽象掉什么的程序员,连问题出在哪都猜不到。

学习时间守恒:抽象替你工作,不替你学习

教 C++ 时最好能跳过 char* 和指针运算,直接教 STL string。但迟早学员会写下 "foo" + "bar"、遇到 OUT LPTSTR 参数,于是必须回头学 char*、指针、Unicode、wchar_t、TCHAR 头文件——所有"漏上来"的东西。

作者自己的例子:第一次实习写 strcat,只需要一本 K&R;多年后做 CityDesk 却要掌握 Visual Basic、COM、ATL、C++、InnoSetup、IE 内部机制、正则、DOM、HTML、CSS、XML。工具全是高层的,但 K&R 的底层仍然不懂不行。

这就是悖论:工具和抽象越来越高级,成为熟练程序员却越来越难——因为你必须同时掌握高层工具,以及它们下面的所有层次。

与"能力 vs 性质"对读

abstraction:抽象设计是双向取舍——能力 ↑ ⇔ 可分析的性质 ↓。泄漏是这枚硬币的另一面:设计阶段失去的是"可分析的性质",运行阶段漏出的是"底层的现实"。二者同源:都是抽象没有覆盖的那部分。合并后的逐系统分析框架(核心抽象三问 × 泄漏四问,含 Redis / MySQL / Kafka / NFS 实例)见 abstraction

启发点(关键洞察):

  1. 泄漏是必然事实,不是实现 bug:抽象是策略性简化,被隐藏的底层失败不会消失,只会被推迟到最出乎意料的时刻。设计抽象时第一问应是"我不保护什么"。
  2. 抽象省工作时间,不省学习时间:能力 = 会用 + 会穿透。学习路线要自底向上完整过一遍,"只学工具"省下的时间会在泄漏时加倍还回去。
  3. 层叠抽象放大人祸:NFS + .forward 事故中每一层都"负责一点",但没有一层负责端到端语义;多层抽象叠加时,端到端不变量必须有人显式负责。
  4. "只要说明、不要过程"的抽象(SQL)泄漏最隐蔽也最贵:结果等价掩盖过程差异,性能责任无法被抽象掉;过程知识是长期执照。
  5. 排障的第一假设是"抽象泄漏了":高层行为异常时沿协议栈向下追,而不是在抽象层反复找不可能的解释。
  6. 设计者应当诚实:契约只承诺能兑现的,已知泄漏点写进文档;使用者学新抽象的第一问是 Where does it break?——这正是 How To Understand Things 的边界问题。
  7. abstraction(能力 vs 性质)对读:设计时失去性质、运行时泄漏底层,是同一个"抽象未覆盖部分"的两面;在 law-sofsoftware-engineering 中与 Tesler 复杂度守恒定律互为印证——复杂度只能转移,不能消灭。
  1. https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions
  2. https://www.tedinski.com/2018/01/30/the-one-ring-problem-abstraction-and-power.html (能力与性质的交换)
  3. https://en.wikipedia.org/wiki/Law_of_leaky_abstractions

评论