The Law of Leaky Abstractions
Joel Spolsky's classic essay starts with TCP, which promises reliable, ordered, uncorrupted delivery on top of IP, a protocol that guarantees none of those things. TCP is an abstraction, and like every non-trivial abstraction, it leaks. The examples span the stack: iterating a 2D array column-wise can trigger far more page faults than row-wise; logically equivalent SQL queries can differ by orders of magnitude in runtime; no C++ string class can make "foo" + "bar" compile, because string literals are char*; an NFS server outage silently drops mail that depended on a .forward file; ASP.NET fakes form submission from a hyperlink with generated onclick JavaScript, breaking when JavaScript is disabled. The practical consequence: abstractions save time writing code, not time learning. As tools get higher-level, debugging them still requires knowing what was abstracted away, so proficiency gets harder, not easier.
There’s a key piece of magic in the engineering of the Internet which you rely on every single day. It happens in the TCP protocol, one of the fundamental building blocks of the Internet.
TCP is a way to transmit data that is reliable. By this I mean: if you send a message over a network using TCP, it will arrive, and it won’t be garbled or corrupted.
We use TCP for many things like fetching web pages and sending email. The reliability of TCP is why every email arrives in letter-perfect condition. Even if it’s just some dumb spam.
互联网工程中有一项你每天都在依赖的关键魔法。它就发生在 TCP 协议里,而 TCP 是互联网最基础的构建块之一。
TCP 是一种可靠的数据传输方式。我的意思是:如果你用 TCP 通过网络发送消息,它一定会到达,而且不会被弄乱或损坏。
我们用 TCP 做很多事情,比如获取网页和发送电子邮件。正是因为 TCP 可靠,每封邮件才能原封不动地送达——哪怕它只是一堆无聊的垃圾邮件。
By comparison, there is another method of transmitting data called IP which is unreliable. Nobody promises that your data will arrive, and it might get messed up before it arrives. If you send a bunch of messages with IP, don’t be surprised if only half of them arrive, and some of those are in a different order than the order in which they were sent, and some of them have been replaced by alternate messages, perhaps containing pictures of adorable baby orangutans, or more likely just a lot of unreadable garbage that looks like that spam you get in a foreign language.
相比之下,还有一种数据传输方式叫 IP,它并不可靠。没人保证你的数据能到达,就算到达了,也可能已经出错。如果你用 IP 发送一堆消息,结果只有一半到达,别惊讶;而且其中一些到达的顺序和发送顺序不同,还有一些被替换成了别的消息——可能是可爱的红毛猩猩幼崽照片,但更可能只是一堆乱码,看起来就像你收到的那种外文垃圾邮件。
Here’s the magic part: TCP is built on top of IP. In other words, TCP is obliged to somehow send data reliably using only an unreliable tool.
神奇的地方在于:TCP 是建立在 IP 之上的。换句话说,TCP 只能用一种不可靠的工具,却必须设法可靠地发送数据。
To illustrate why this is magic, consider the following morally equivalent, though somewhat ludicrous, scenario from the real world.
Imagine that we had a way of sending actors from Broadway to Hollywood that involved putting them in cars and driving them across the country. Some of these cars crashed, killing the poor actors. Sometimes the actors got drunk on the way and shaved their heads or got nasal tattoos, thus becoming too ugly to work in Hollywood, and frequently the actors arrived in a different order than they had set out, because they all took different routes. Now imagine a new service called Hollywood Express, which delivered actors to Hollywood, guaranteeing that they would (a) arrive (b) in order (c) in perfect condition. The magic part is that Hollywood Express doesn’t have any method of delivering the actors, other than the unreliable method of putting them in cars and driving them across the country. Hollywood Express works by checking that each actor arrives in perfect condition, and, if he doesn’t, calling up the home office and requesting that the actor’s identical twin be sent instead. If the actors arrive in the wrong order Hollywood Express rearranges them. If a large UFO on its way to Area 51 crashes on the highway in Nevada, rendering it impassable, all the actors that went that way are rerouted via Arizona and Hollywood Express doesn’t even tell the movie directors in California what happened. To them, it just looks like the actors are arriving a little bit more slowly than usual, and they never even hear about the UFO crash.
为了说明这为什么神奇,不妨看看现实世界中一个道德上等价、但有点荒唐的场景。
假设我们有一种把演员从百老汇送到好莱坞的方法:让他们坐进汽车,一路开过整个国家。有些车会撞毁,可怜的演员因此丧命。有时演员在路上喝醉了,剃了光头或纹了鼻部纹身,变得太丑,没法在好莱坞工作;而且他们经常不会按出发顺序到达,因为大家走的是不同路线。现在想象一项叫 Hollywood Express 的新服务,它把演员送到好莱坞,并保证他们 (a) 到达 (b) 按顺序 (c) 状态完美。神奇的是,Hollywood Express 除了用那种不可靠的方式——让演员坐车横穿全国——之外,没有任何别的运送办法。Hollywood Express 的做法是:检查每个演员是否状态完美到达;如果没有,就打电话回总部,要求把这位演员的同卵双胞胎送来替代。如果演员到达顺序错了,Hollywood Express 会重新排好。如果一架飞往 51 区的大型 UFO 在内华达州高速公路上坠毁,导致道路无法通行,所有走那条路的演员都会被改道经过亚利桑那州,而 Hollywood Express 甚至不会告诉加州那些电影导演发生了什么。在他们看来,演员只是比平时到得稍慢一点,他们根本听不到 UFO 坠毁这回事。
That is, approximately, the magic of TCP. It is what computer scientists like to call an abstraction: a simplification of something much more complicated that is going on under the covers. As it turns out, a lot of computer programming consists of building abstractions. What is a string library? It’s a way to pretend that computers can manipulate strings just as easily as they can manipulate numbers. What is a file system? It’s a way to pretend that a hard drive isn’t really a bunch of spinning magnetic platters that can store bits at certain locations, but rather a hierarchical system of folders-within-folders containing individual files that in turn consist of one or more strings of bytes.
这大致就是 TCP 的魔法。计算机科学家喜欢把它称为一种抽象:把底层复杂得多的过程简化掉。事实证明,大量计算机编程工作就是在构建抽象。字符串库是什么?它是一种假装计算机操作字符串和操作数字一样容易的方式。文件系统是什么?它是一种假装硬盘并不是一堆旋转的磁性盘片、在特定位置存储比特,而是一个层级化的文件夹套文件夹系统,里面装着一个个文件,而文件又由一个或多个字节串组成。
Back to TCP. Earlier for the sake of simplicity I told a little fib, and some of you have steam coming out of your ears by now because this fib is driving you crazy. I said that TCP guarantees that your message will arrive. It doesn’t, actually. If your pet snake has chewed through the network cable leading to your computer, and no IP packets can get through, then TCP can’t do anything about it and your message doesn’t arrive. If you were curt with the system administrators in your company and they punished you by plugging you into an overloaded hub, only some of your IP packets will get through, and TCP will work, but everything will be really slow.
回到 TCP。前面为了简单起见,我撒了个小谎,现在有些人已经气得七窍生烟,因为这个谎让他们抓狂。我说 TCP 保证你的消息一定会到达。实际上并不是。如果你的宠物蛇咬断了连接电脑的网线,没有任何 IP 数据包能通过,那 TCP 也无能为力,你的消息就是到不了。如果你对公司系统管理员说话太冲,他们惩罚你,把你插到一个过载的 hub 上,那只有部分 IP 数据包能通过,TCP 仍然能工作,但一切都慢得要命。
This is what I call a leaky abstraction. TCP attempts to provide a complete abstraction of an underlying unreliable network, but sometimes, the network leaks through the abstraction and you feel the things that the abstraction can’t quite protect you from. This is but one example of what I’ve dubbed the Law of Leaky Abstractions:
All non-trivial abstractions, to some degree, are leaky.
这就是我所说的泄漏抽象。TCP 试图为底层不可靠网络提供一个完整的抽象,但有时网络会从抽象中泄漏出来,让你感受到抽象没法完全保护你免受的那些东西。这只是一个例子,我把它称为泄漏抽象定律:
所有非平凡的抽象,在某种程度上都会泄漏。
Abstractions fail. Sometimes a little, sometimes a lot. There’s leakage. Things go wrong. It happens all over the place when you have abstractions. Here are some examples.
Something as simple as iterating over a large two-dimensional array can have radically different performance if you do it horizontally rather than vertically, depending on the “grain of the wood” — one direction may result in vastly more page faults than the other direction, and page faults are slow. Even assembly programmers are supposed to be allowed to pretend that they have a big flat address space, but virtual memory means it’s really just an abstraction, which leaks when there’s a page fault and certain memory fetches take way more nanoseconds than other memory fetches.
抽象会失败。有时只漏一点,有时漏很多。会有泄漏。事情会出错。只要你使用抽象,这种事就无处不在。下面是一些例子。
像遍历一个大型二维数组这样简单的事,如果你水平遍历而不是垂直遍历,性能就可能天差地别,这取决于“木纹的方向”——一个方向可能导致多得多的页错误,而页错误很慢。即便是汇编程序员,按理说也可以假装自己拥有一个巨大而平坦的地址空间,但虚拟内存意味着这其实只是一种抽象;当发生页错误、某些内存读取比其他内存读取多花很多纳秒时,抽象就泄漏了。
The SQL language is meant to abstract away the procedural steps that are needed to query a database, instead allowing you to define merely what you want and let the database figure out the procedural steps to query it. But in some cases, certain SQL queries are thousands of times slower than other logically equivalent queries. A famous example of this is that some SQL servers are dramatically faster if you specify “where a=b and b=c and a=c” than if you only specify “where a=b and b=c” even though the result set is the same. You’re not supposed to have to care about the procedure, only the specification. But sometimes the abstraction leaks and causes horrible performance and you have to break out the query plan analyzer and study what it did wrong, and figure out how to make your query run faster.
SQL 语言的目的,是抽象掉查询数据库所需的程序步骤,让你只定义想要什么,然后让数据库自己找出查询的程序步骤。但在某些情况下,某些 SQL 查询会比逻辑上等价的其他查询慢上千倍。一个著名的例子是:有些 SQL 服务器在你指定 “where a=b and b=c and a=c” 时,会比只指定 “where a=b and b=c” 快得多,哪怕结果集完全相同。按理说你不该关心过程,只该关心规格说明。但有时抽象会泄漏,导致性能糟糕透顶,你不得不掏出查询计划分析器,研究它到底哪里做错了,再想办法让查询跑得更快。
Even though network libraries like NFS and SMB let you treat files on remote machines “as if” they were local, sometimes the connection becomes very slow or goes down, and the file stops acting like it was local, and as a programmer you have to write code to deal with this. The abstraction of “remote file is the same as local file” leaks. Here’s a concrete example for Unix sysadmins. If you put users’ home directories on NFS-mounted drives (one abstraction), and your users create .forward files to forward all their email somewhere else (another abstraction), and the NFS server goes down while new email is arriving, the messages will not be forwarded because the .forward file will not be found. The leak in the abstraction actually caused a few messages to be dropped on the floor.
像 NFS 和 SMB 这样的网络库,让你可以把远程机器上的文件“当作”本地文件来对待,但有时连接会变得很慢或直接断开,文件就不再表现得像本地文件,作为程序员,你必须写代码来处理这种情况。“远程文件等同于本地文件”这个抽象泄漏了。这里有一个给 Unix 系统管理员的具体例子。如果你把用户的主目录放在 NFS 挂载的驱动器上(一个抽象),而用户又创建 .forward 文件把所有邮件转发到别处(另一个抽象),当新邮件到达时 NFS 服务器宕机了,那么这些邮件就不会被转发,因为找不到 .forward 文件。抽象中的泄漏实际上导致一些邮件被丢在地上。
C++ string classes are supposed to let you pretend that strings are first-class data. They try to abstract away the fact that strings are hard and let you act as if they were as easy as integers. Almost all C++ string classes overload the + operator so you can write s + “bar” to concatenate. But you know what? No matter how hard they try, there is no C++ string class on Earth that will let you type “foo” + “bar”, because string literals in C++ are always char*’s, never strings. The abstraction has sprung a leak that the language doesn’t let you plug. (Amusingly, the history of the evolution of C++ over time can be described as a history of trying to plug the leaks in the string abstraction. Why they couldn’t just add a native string class to the language itself eludes me at the moment.)
C++ 的 string 类本想让你假装字符串是一等数据。它们试图抽象掉字符串很难处理这一事实,让你把它们用得和整数一样简单。几乎所有 C++ string 类都重载了 + 运算符,所以你可以写 s + “bar” 来拼接。但你知道吗?不管它们多努力,地球上没有任何一个 C++ string 类能让你写 “foo” + “bar”,因为 C++ 里的字符串字面量永远是 char*,从来不是 string。这个抽象裂开了一个语言不让你补的洞。(有意思的是,C++ 随时间演化的历史,可以描述成一部试图堵上字符串抽象漏洞的历史。至于他们为什么不干脆给语言本身加一个原生 string 类,我现在也搞不明白。)
And you can’t drive as fast when it’s raining, even though your car has windshield wipers and headlights and a roof and a heater, all of which protect you from caring about the fact that it’s raining (they abstract away the weather), but lo, you have to worry about hydroplaning (or aquaplaning in England) and sometimes the rain is so strong you can’t see very far ahead so you go slower in the rain, because the weather can never be completely abstracted away, because of the law of leaky abstractions.
下雨时你没法开得那么快,哪怕你的车有雨刷、车灯、车顶和暖气,这些东西都让你不必在意下雨这件事(它们把天气抽象掉了)。可是瞧,你还得担心车辆打滑(在英国叫 aquaplaning),有时雨大到你根本看不清前方,所以雨天只能开慢点。因为天气永远不可能被完全抽象掉,这就是泄漏抽象定律。
One reason the law of leaky abstractions is problematic is that it means that abstractions do not really simplify our lives as much as they were meant to. When I’m training someone to be a C++ programmer, it would be nice if I never had to teach them about char*’s and pointer arithmetic. It would be nice if I could go straight to STL strings. But one day they’ll write the code “foo” + “bar”, and truly bizarre things will happen, and then I’ll have to stop and teach them all about char*’s anyway. Or one day they’ll be trying to call a Windows API function that is documented as having an OUT LPTSTR argument and they won’t be able to understand how to call it until they learn about char*’s, and pointers, and Unicode, and wchar_t’s, and the TCHAR header files, and all that stuff that leaks up.
泄漏抽象定律之所以麻烦,原因之一是:抽象并没有像原本设想的那样真正简化我们的生活。当我培训一个人成为 C++ 程序员时,如果我永远不用教他们 char* 和指针运算,那该多好。如果我能直接讲 STL string,那该多好。但总有一天他们会写出 “foo” + “bar” 这样的代码,然后发生极其诡异的事情,于是我还是得停下来,把 char* 从头到尾讲一遍。又或者有一天,他们想调用一个文档里写着 OUT LPTSTR 参数的 Windows API 函数,却怎么都不明白该怎么调用,直到他们学会了 char*、指针、Unicode、wchar_t、TCHAR 头文件,以及所有那些泄漏出来的东西。
In teaching someone about COM programming, it would be nice if I could just teach them how to use the Visual Studio wizards and all the code generation features, but if anything goes wrong, they will not have the vaguest idea what happened or how to debug it and recover from it. I’m going to have to teach them all about IUnknown and CLSIDs and ProgIDS and … oh, the humanity!
在教别人 COM 编程时,如果我能只教他们怎么用 Visual Studio 向导和各种代码生成功能,那该多好。可一旦出了任何问题,他们压根不知道发生了什么、该如何调试和恢复。我最后还是得把 IUnknown、CLSID、ProgID 等等全都教一遍……天哪,饶了我吧!
In teaching someone about ASP.NET programming, it would be nice if I could just teach them that they can double-click on things and then write code that runs on the server when the user clicks on those things. Indeed ASP.NET abstracts away the difference between writing the HTML code to handle clicking on a hyperlink (<a>) and the code to handle clicking on a button. Problem: the ASP.NET designers needed to hide the fact that in HTML, there’s no way to submit a form from a hyperlink. They do this by generating a few lines of JavaScript and attaching an onclick handler to the hyperlink. The abstraction leaks, though. If the end-user has JavaScript disabled, the ASP.NET application doesn’t work correctly, and if the programmer doesn’t understand what ASP.NET was abstracting away, they simply won’t have any clue what is wrong.
在教别人 ASP.NET 编程时,如果我能只告诉他们:你可以双击某些东西,然后写代码,让用户在点击这些东西时在服务器上运行,那该多好。确实,ASP.NET 把编写 HTML 代码来处理点击超链接(<a>)和编写代码来处理点击按钮之间的差异抽象掉了。问题在于:ASP.NET 的设计者需要隐藏一个事实——在 HTML 里,没办法从超链接提交表单。他们的做法是生成几行 JavaScript,并给超链接附加一个 onclick 处理函数。可是这个抽象泄漏了。如果最终用户禁用了 JavaScript,ASP.NET 应用就无法正常工作;而如果程序员不理解 ASP.NET 到底抽象掉了什么,他们根本不会有任何线索知道问题出在哪。
The law of leaky abstractions means that whenever somebody comes up with a wizzy new code-generation tool that is supposed to make us all ever-so-efficient, you hear a lot of people saying “learn how to do it manually first, then use the wizzy tool to save time.” Code generation tools which pretend to abstract out something, like all abstractions, leak, and the only way to deal with the leaks competently is to learn about how the abstractions work and what they are abstracting. So the abstractions save us time working, but they don’t save us time learning.
泄漏抽象定律意味着:每当有人推出一个花哨的新代码生成工具,号称能让我们所有人效率高得不得了,你就会听到很多人说:“先学会手动做,再用这个花哨工具省时间。” 那些假装抽象掉某些东西的代码生成工具,和所有抽象一样,会泄漏;要能胜任地处理这些泄漏,唯一的办法就是理解抽象如何工作、它们抽象掉了什么。所以,抽象节省了我们工作的时间,却没有节省我们学习的时间。
And all this means that paradoxically, even as we have higher and higher level programming tools with better and better abstractions, becoming a proficient programmer is getting harder and harder.
而这一切意味着一个悖论:即便我们拥有越来越高级的编程工具、越来越好的抽象,要成为一名熟练的程序员,反而越来越难。
During my first Microsoft internship, I wrote string libraries to run on the Macintosh. A typical assignment: write a version of strcat that returns a pointer to the end of the new string. A few lines of C code. Everything I did was right from K&R — one thin book about the C programming language.
Today, to work on CityDesk, I need to know Visual Basic, COM, ATL, C++, InnoSetup, Internet Explorer internals, regular expressions, DOM, HTML, CSS, and XML. All high level tools compared to the old K&R stuff, but I still have to know the K&R stuff or I’m toast.
我在微软第一次实习时,为 Macintosh 写字符串库。一个典型任务是:写一个 strcat 版本,让它返回新字符串末尾的指针。几行 C 代码而已。我做的一切都直接来自 K&R——一本讲 C 语言的薄薄小书。
今天,为了开发 CityDesk,我需要懂 Visual Basic、COM、ATL、C++、InnoSetup、Internet Explorer 内部机制、正则表达式、DOM、HTML、CSS 和 XML。和当年 K&R 那些东西比起来,这些都算高级工具,但我仍然必须懂 K&R 那些东西,否则就完蛋了。
Ten years ago, we might have imagined that new programming paradigms would have made programming easier by now. Indeed, the abstractions we’ve created over the years do allow us to deal with new orders of complexity in software development that we didn’t have to deal with ten or fifteen years ago, like GUI programming and network programming. And while these great tools, like modern OO forms-based languages, let us get a lot of work done incredibly quickly, suddenly one day we need to figure out a problem where the abstraction leaked, and it takes 2 weeks. And when you need to hire a programmer to do mostly VB programming, it’s not good enough to hire a VB programmer, because they will get completely stuck in tar every time the VB abstraction leaks.
十年前,我们也许想象过,新的编程范式到如今会让编程变得更容易。确实,这些年来我们创造的抽象,让我们能够处理十年前或十五年前不必面对的、更高数量级的软件开发复杂度,比如 GUI 编程和网络编程。虽然这些强大的工具——比如现代基于表单的面向对象语言——让我们能极其快速地完成大量工作,但突然有一天,我们需要解决一个抽象泄漏的问题,结果一花就是两周。而当你需要雇一个主要做 VB 编程的程序员时,只雇一个 VB 程序员是不够的,因为每次 VB 抽象泄漏,他们都会彻底陷进焦油里动弹不得。
The Law of Leaky Abstractions is dragging us down.
泄漏抽象定律正在拖我们的后腿。