Non-Profit, International

Spirit unsterblich.

抽象泄漏定律

字数统计:3879 blog

所有重要的抽象,在一定程度上,都是有漏洞的。

原文:The Law of Leaky Abstractions

译文:抽象泄漏定律

你每天依赖的互联网工程中有一些关键的小魔法。这些魔法发生在 TCP 协议中,它是组成互联网的重要基石。

TCP 是一种可靠的数据传输方式。我的意思是:如果你在网络中使用 TCP 来发送消息,它一定会到达,并且不会出现乱码和损坏。

我们通过 TCP 进行了诸如打开网页或者发送邮件等许多活动。TCP 的可靠性,是之所以每封邮件都可以完美地送达的原因。即便它只是一些垃圾邮件。

作为对比, 有一另外一种叫做 IP 协议的数据传输方法就不那么可靠了。在这个协议下,没有人会承诺你数据一定会到达,而且数据有可能在到达之前被破坏。如果你通过 IP 协议发送了一串数据,如果发生只有一半到达的情况,请不要感到惊讶,而且相比发送的时候,数据的顺序也可能发生改变,并且其中有些还可能被替换成其他的消息,有可能是一些可爱的猩猩照片,或者更可能是一些无法阅读的垃圾,就像你收到的外语垃圾邮件一般。

这就是魔法的一部分了:TCP 协议是在 IP 协议之上构建的。换句话说,TCP 使用了一个不可靠的工具来让数据传输变得可靠。

为了表明为什么这是一个魔法,考虑以下真实世界里关于道德上等价的场景,尽管它听起来有些荒谬。

想象我们有一个方法将一些演员从百老汇送到好莱坞,包括将它们放在一个车里并载着他们穿越全国。有一些车会在途中坠毁,车中可怜的演员都挂掉了。而有些演员在途中会喝醉了酒,并剃了光头或纹身,从而变得太丑而无法在好莱坞工作,并且演员到达的顺序经常和他们出发的顺序不一样,因为他们都选了不同的路线过来。现在想象有一种新的服务叫做好莱坞快递,它可以保证演员完美地按照一定顺序到达好莱坞。神奇的部分是,好莱坞快递除了上述将演员放在汽车里穿越全国这种不可靠的方法外,并没有其他送达演员的方法。好莱坞快递的做法是,检查每个演员是否按照条件完美地到达了,如果没有的话,就会致电演员所在的办公室,并要求将演员的同卵双胞胎送来。如果演员到达的顺序错了,好莱坞快递则会重排他们。如果在途径内华达的 51 区中,有一个巨大的 UFO 毁坏了高速公路,使其无法通行,那么所有的演员都会改道亚利桑那州,并且好莱坞快递甚至不会告诉加州的导演到底发生了什么。靠这些,使得演员看起来比平常到的慢了一些,但是他们再也不会听说关于 UFO 攻击的事情了。

这大概就是 TCP 的神奇之处。 它就是计算机科学家喜欢称之为抽象的东西:对隐藏在幕后的更复杂事物的简化。 事实证明,许多计算机编程都包括构建抽象。 什么是字符串库? 这是一种假装计算机可以像处理数字一样轻松地处理字符串的方法。 什么是文件系统? 这是一种假装硬盘驱动器看起来不只是一堆可以在某些位置存储 bit 的可以旋转的磁盘的方法,而是一个个文件夹的分层系统,其中包含单个文件,这些文件由一个或多个字节的字符串组成。

回到 TCP。 之前为了简单起见,我说了一个小谎言,我相信你们中的一些人已经发现了,因为这个谎言足以让你发疯。 我说 TCP 保证你的消息会到达。 事实并非如此。 如果你的宠物蛇咬断了连着你电脑的网线,没有 IP 包可以通过,那么 TCP 也就无能为力了,你的消息并不会到达。 如果你与公司的系统管理员相处得不太好,他们通过将你插入过载的集线器来惩罚您,那么你的 IP 数据包就只有部分可以通过, 虽然 TCP 依然可以工作,但一切都会非常缓慢。

这就是我称之的抽象泄漏。TCP 在不可靠的网络上提供完整的抽象,但有些时候,网络穿过抽象泄漏了出来,让你觉得抽象不再能悄悄地保护你。这就是一个我所说的抽象泄漏定律的一个例子:

所有重要的抽象,在一定程度上,都是有漏洞的。

抽象的失败。或多或少都会有一些漏洞或者错误。只要你进行抽象,这总是会发生。这有一些例子:

  • 有些时候简单如遍历两个大的二维数组的事情,如果你选择了横向遍历而不是纵向遍历,会有完全不一样的性能,这取决于“木纹” —— 一个方向上可能会导致比另外一个方向上的页面错误更多,并且更慢。即使汇编语言程序员可以被允许假装拥有很大一个平坦的地址空间,但虚拟内存也是一种抽象,当页面发生错误或者真实内存读取数据的方法比其他的内存读取慢几秒,也会发生抽象泄漏。

  • SQL 语言旨在抽象出查询数据库所需的过程步骤,可以让你仅仅通过定义就可以让数据库识别出你需要的查找数据的程序步骤。但在一些情况下,标准 SQL 查询要比其他逻辑等价的查询慢上几千倍。一个著名的例子是,一些 SQL 服务器在你指定 "where a=b and b=c and a=c" 的时候,会比 where a=b and b=c 出人意料地快,尽管它们的结果是相同的。你并不应该去关心程序,而应该只关心规范。但是有些时候抽象泄漏,并导致了严重的性能问题,以至于你不得不中断查询分析,而去找寻发生了什么错误,以便于找出让你的查询变得更快的方法。

  • 尽管一些类似 NFS 或者 SMB 之类的网络库可以让你“仿佛”在本地一样去访问远程的文件,但有些时候网络连接会变慢或者断掉,这时候文件就不像是本地的了,作为一个程序员,你还不得不写一些代码来对付这种情况。“让远程文件和本地文件一样”的抽象泄漏了。在 Unix 系统管理中有一些更明确的例子。如果你把用户的根目录放在挂载着 NFS 的磁盘上(一个抽象),并且你的用户创建了一些 .forward 文件来指向他们别处的 email (另一个抽象),当 NFS 服务器挂掉,并且这时有新的邮件到达,邮件消息将不会发出,因为 .forward 文件无法找到。这个抽象中的泄漏会导致一些消息被丢失掉。

  • C++ 的字符串类让你可以假装字符串时一个原始类数据。它们试图将字符串中很难的部分抽象出来,以便于然给你如同对待整数一般简单地对待它们。几乎所有的 C++ 字符串来都重载饿了 + 操作符,所以你可以写出 s+"bar" 这样的语句来连接字符串。但是你知道吗?不管它们多努力,地球上所有的 C++ 字符串类都不能让你写出类似 "for" + "bar" 这样的语句,因为在 C++ 中字符串总是字符指针(char*),而不是真正的字符串。 因为语言不让你这么相加,抽象产生了一个漏洞。(有趣的是,在 C++ 发展的历史上,一直在尝试修复这个字符串抽象的漏洞。我一直不能理解他们为什么不能在语言中添加一个原生字符串类。(实际上是因为 C++ 为了保持和 C 的兼容性,不能擅自将字符串数组提升为字符串或者字符串常量,C++11 增加了 string literal 以及 C++17 增加了 string_view literal 弥补了这个缺陷))

  • 你不能在下雨天开车开的太快,尽管你的挡风玻璃雨刮、前灯、车顶及加热器一起来保护你免于担心下雨的事实(它们抽象了天气),但是你还是得担心车在水中打滑(或者说英格兰滑水),或者说有些时候雨太大了,你不能看清远处的东西,所以只能慢慢开,这一切都是因为天气永远不能被完整地抽象掉,抽象泄漏定律一直存在。

抽象泄漏定律之所以会成为一个问题,是因为它意味着抽象并不能如同它们期望的那样简化我们的生活。当我训练一个人成为一个 C++ 程序员,如果我不需要指导他们关于 char* 或者指针计算就好了。如果我可以直接使用 STL 字符串就好了。但是有一天它们会写出 "foo" + "bar" 这样的代码,那么真正可怕的事情就发生了,我将不得不停止工作,还是要去教会他们所有关于 char* 的知识。有一天他们会去尝试调用 Windows API 函数,文档中标明这个函数有一个 OUT LPSTR类型的参数,如果他们没学习过关于 char* 和指针的知识, 他们就无法理解,这还包括了 Unicode、wchar_t、 头文件中的 TCHAR,所有这些都会造成抽象泄漏。

在教人们做 COM 编程的时候,如果我可以只教他们如何使用 Visual Studio 向导并生成所有代码的功能就好了,但如果一旦发生了错误,他们就完全不知道发生了什么,更不要提 debug 和修复它了。我只能教他们所有关于 IUnknown 和 CLSID 以及 ProgIDS 等等一堆知识…啊,人类啊!

在教人们关于 ASP.NET 变成的时候,如果我只需要教他们双击,并且在其上写下用户点击时在服务器上运行的代码就好了。事实上 ASP.NET 将写 HTML 代码中响应超链接(<a>)点击或响应按钮点击抽象了出来。问题是:ASP.NET 的设计者需要隐藏真实的 HTML,所以就没法在超链接里去提交表单了。他们通过生成了一些 JavaScript 代码加到超链接的 onclick 事件上来达到这个效果。但是这个抽象泄漏了。如果一个终端用户禁止了 JavaScript,那么 ASP.NET 应用将不会正常工作,并且如果程序员还不了解 ASP.NET 的这些抽象的话,他们就完全不知道错误发生在哪里了。

抽象泄漏定律意味着,每当有人想出了一个令人惊叹的代码生成工具试图来让我们更有效率的时候,你会听到很多人说“先学会如何手动处理,再使用这些工具来节省时间。”代码生成工具会假装抽象了一些事,但所有的抽象都会泄漏,唯一应对的办法就是学习这些抽象是如何工作的,以及它们抽象了哪些东西。所以抽象可能会节省我们工作的时间,但不会节省我们学习的时间。

这一切仿佛是个悖论,尽管我们有越来越高级的编程工具,抽象也做的越来越好,但成为一个精通的程序员将会越来越难。

在我最初在微软实习的时候,我写了一些可以在苹果电脑上运行的库。一个典型是:写一个新的 strcat 可以返回新字符串结尾的指针。 我通过学习一本很薄的 C 语言编程书籍 —— K&R 就写下了这几行 C 代码。

如今,在 CityDesk 工作的期间,我需要知道 Visual Basic、COM、ATL、C++、InnoStep、IE 内核、正则表达式、DOM、HTML、CSS 以及 XML 等等…所有这些高级的东西都可以比拟老的 K&R,但我还是必须要了解 K&R,否则我就被淘汰了。

几年前,我可能会想象新的编程范式可能会使编程比现在更简单。事实上,我们多年来创建的抽象确实让我们能够处理软件开发中新的复杂任务,让我们不需要再像 10 或 15 年前去那样对付它们,比如 GUI 编程和网络编程。而且通过一些诸如现代面向对象编程语言这一类很棒的工具,让我们的许多工作变得不可思议的快捷,但当某天我们需要找出抽象泄漏引起的问题时,却需要花至少 2 周的时间。当你需要雇佣一个程序员来做 VB 编程的时候,只有一个 VB 程序员是不够的,因为当 VB 发生抽象泄漏的时候,他就会被彻底地卡在那里了。

抽象泄漏定律让我们慢下来。


若无特殊声明,本文以 CC BY-SA 3.0 许可协议 提供。