抽象泄露法则
在 AI 时代,开发效率大幅提升。某团队借助 AI 辅助开发,仅用 5 分钟便完成了一个新功能的开发,初看运行正常。然而,在某些特定场景下,功能却出现了意料之外的问题。团队花了半天时间排查,才发现问题的根源深藏于底层实现之中——本以为可以忽略的细节,最终却成了最大的坑。
另一边,某系统突发线上故障,业务开发人员求助平台开发人员,期待快速定位问题。然而,平台开发人员查看后表示:“底层代码没有问题,业务侧再看看。” 业务开发团队只好继续摸索,试图在复杂的调用链与抽象层中找到线索。原本清晰分工的技术栈,在关键时刻变得模糊不清,底层的复杂性不可避免地泄露出来。
软件世界就是一层抽象套着另一层抽象的千层饼,就好像 HTTP 协议下有 TCP、TCP 下有 IP,每一层抽象都声称自己是完美的:“你无需关注在我之下的任何细节”。
但事实却是,所有抽象必定泄露。而当抽象泄露时,就像要从 AI 生成的 1000 行代码里找到那个错误——事情非常棘手,但我们别无选择。
早在 2002 年,程序员 Joel Spolsky 就敏锐地发现了这类现象,并将它们总结为:“抽象泄露法则”。
译文如下:
抽象泄露法则 作者:Joel Spolsky
日期:2002年11月11日,星期一
互联网工程中有一项关键的魔法,你每天都在依赖它。这种魔法发生在TCP协议中,TCP是互联网的基础构建模块之一。
TCP是一种可靠的数据传输方式。我的意思是:如果你通过网络使用TCP发送一条消息,它将会到达目的地,并且不会出现乱码或损坏。
我们使用TCP来做很多事情,比如获取网页和发送电子邮件。TCP的可靠性确保了每一封来自东非诈骗者的激动人心的电子邮件都能以完美的状态送达。哦,真是令人欣喜。
相比之下,还有一种称为IP的数据传输方式,它是不可靠的。没有人保证你的数据会到达目的地,而且数据在到达之前可能会被搞乱。如果你用IP发送一堆消息,不要惊讶只有一半的消息到达,而且其中一些消息的顺序与发送时的顺序不同,还有一些消息可能被替换为其他内容,或许是可爱的猩猩宝宝图片,或者更可能只是一堆无法阅读的垃圾,看起来像台湾垃圾邮件的主题行。
这里的神奇之处在于:TCP是建立在IP之上的。换句话说,TCP必须通过一种不可靠的工具来可靠地发送数据。
为了说明这为什么是魔法,请考虑以下道德上等效但有些荒谬的现实世界场景。
想象一下,我们有一种将百老汇演员送到好莱坞的方式,方法是把他们塞进车里,然后开车穿越全国。有些车会撞毁,可怜的演员因此丧生。有时演员在路上喝醉了,剃了光头或纹了鼻环,结果变得太丑无法在好莱坞工作。而且,演员们到达的顺序通常与他们出发的顺序不同,因为他们都走了不同的路线。现在想象一种名为“好莱坞快线”的新服务,它保证将演员送到好莱坞,并确保他们(a)到达,(b)按顺序到达,(c)状态完好。神奇之处在于,好莱坞快线没有任何其他方法可以运送演员,除了将他们塞进车里并开车穿越全国这种不可靠的方式。好莱坞快线的工作原理是检查每个演员是否完好无损地到达,如果没有,就打电话给总部,要求发送该演员的同卵双胞胎代替。如果演员到达的顺序不对,好莱坞快线会重新排列他们。如果一架飞往51区的大型UFO在内华达州的高速公路上坠毁,导致道路无法通行,所有走那条路的演员都会通过亚利桑那州重新安排路线,而好莱坞快线甚至不会告诉加利福尼亚的电影导演发生了什么。对他们来说,演员只是比平时到得慢了一点,他们甚至从未听说过UFO坠毁事件。
这大致就是TCP的魔法。它是计算机科学家喜欢称之为“抽象”的东西:一种对底层复杂事物的简化。事实证明,许多计算机编程工作都涉及构建抽象。什么是字符串库?它是一种假装计算机可以像操作数字一样轻松操作字符串的方式。什么是文件系统?它是一种假装硬盘并不是一堆可以存储比特的旋转磁性盘片,而是一个由文件夹嵌套文件夹组成的层次系统,其中包含由一或多个字节串组成的单个文件。
回到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文件以将所有电子邮件转发到其他地方(另一种抽象),当新邮件到达时,如果NFS服务器宕机,邮件将不会被转发,因为.forward文件将无法找到。抽象中的泄漏实际上导致了一些邮件被丢弃。
C++字符串类本应让你假装字符串是一等数据。它们试图抽象出字符串的复杂性,让你感觉它们像整数一样容易操作。几乎所有的C++字符串类都重载了+操作符,因此你可以写s + “bar"来进行连接。但你知道吗?无论它们多么努力,地球上没有任何一个C++字符串类能让你输入"foo” + “bar”,因为C++中的字符串字面量始终是char*,而不是字符串。抽象出现了泄漏,而语言本身不允许你修补它。(有趣的是,C++的演变历史可以被描述为试图修补字符串抽象泄漏的历史。为什么他们不能在语言本身中添加一个原生的字符串类,这让我一时难以理解。)
而且,下雨时你也不能开得太快,尽管你的车有雨刷、头灯、车顶和暖气,这些都让你不必关心下雨的事实(它们抽象了天气),但瞧,你必须担心打滑(在英国称为“水上漂”),有时雨太大,你看不清前方,所以你只能在雨中开得更慢,因为天气永远无法被完全抽象掉,这就是抽象泄漏法则的作用。
抽象泄漏法则之所以成问题,原因之一是它意味着抽象并没有像预期那样真正简化我们的生活。当我培训某人成为C++程序员时,如果我能永远不必教他们关于char和指针运算的知识,那将是非常好的。如果我能直接跳到STL字符串,那将是非常好的。但有一天他们会写"foo" + “bar"这样的代码,然后真正奇怪的事情会发生,那时我将不得不停下来教他们所有关于char的知识。或者有一天,他们会尝试调用一个Windows API函数,该函数的文档显示它有一个OUT LPTSTR参数,直到他们学习了char*、指针、Unicode、wchar_t和TCHAR头文件等知识,他们才能理解如何调用它。所有这些泄漏都会暴露出来。
在教某人COM编程时,如果我能只教他们如何使用Visual Studio向导和所有代码生成功能,那将是非常好的。但如果出现问题,他们将完全不知道发生了什么,也不知道如何调试和恢复。我将不得不教他们所有关于IUnknown、CLSID、ProgID等的知识……哦,真是让人头疼!
在教某人ASP.NET编程时,如果我能只教他们可以双击某些东西,然后编写在用户点击这些内容时在服务器上运行的代码,那将是非常好的。事实上,ASP.NET抽象了处理点击超链接(<a>)的HTML代码和处理点击按钮的代码之间的区别。问题是:ASP.NET的设计者需要隐藏一个事实,即在HTML中,没有办法通过超链接提交表单。他们通过生成几行JavaScript代码并将onclick处理程序附加到超链接上来实现这一点。然而,抽象出现了泄漏。如果最终用户禁用了JavaScript,ASP.NET应用程序将无法正常工作,如果程序员不理解ASP.NET抽象了什么,他们将完全不知道哪里出了问题。
抽象泄漏法则意味着,每当有人提出一个花哨的新代码生成工具,声称能让我们变得非常高效时,你会听到很多人说:“先学会手动操作,然后再用花哨的工具来节省时间。”那些假装抽象某些东西的代码生成工具,像所有抽象一样,都是有泄漏的,而唯一能有效处理这些泄漏的方法是学习抽象的工作原理以及它们抽象了什么。因此,抽象节省了我们工作的时间,但它们并没有节省我们学习的时间。
这一切意味着,尽管我们有了越来越高级的编程工具和越来越好的抽象,但成为一名熟练的程序员却变得越来越难。