2019年/11月/01日
抽象必然泄露
这是一个定律,好遗憾我知道得太晚了,早点明白这个定律,早点能够醒悟工作中的那么多烦心事,下面这个列表曾经让我无比困惑:
比如这个SQL怎么这么慢?
怎么ES的数据平衡代价这么高?
怎么隔离级别有这么多套路?
怎么Hibernate如此的复杂?你知道OSIV吗?
实现一个Redis锁为什么如此之难?
为什么Seam死了?
Java的泛型的坑你了解多少?
构造一个可靠的线程池怎么也很难?
你能优雅的用SQL查询ES,Solr,Hadoop?
你想通过一个代码生成器来解决复杂业务问题?
你会写Js吗?Function.constructor.constructor.constructor == Function,这个你能理解吗?
NIO就一定比BIO高级?
不引入一个Node?你还能愉快的写前端?
一个logger.debug("")可以把系统弄挂掉
为什么大量的开发不知道equlas和hashcode的关系?
@Transactional标在方法上的巨大恐怖性
啥叫伪共享?
你看到的真的是事实吗?
这个列表还有很长,等我退休了把他全部打印出来回忆,哈哈.
如果你还没觉得抽象泄露有多可怕,看看CPU的bug:链接
你被浮点折磨过吗?
Double.MAX_VALUE == Double.MAX_VALUE + 100 //true
0.99999999f==1f //true
0.9f==1f //false
我一直讨厌SpringBoot和Mybatis,也是因为受到这个定律的影响,因为抽象必然泄露。 Effective Java整本书,都是在警告我们,需要不做什么,类似这样的书还有《Java安全编码标准》,这些书背后都是抽象泄露定律作怪。
怎么办呢?我们只能寻找简洁透明的东西,因为越简洁泄露可能才会越少,要不做什么, 因为不做什么比做什么更有学问
透明要理解为足够的透明和足够的不透明,因为当足够的不透明之后就等于透明了,比如CPU
这个定律的提出者是:Joel Spolsky, 出自于他的书:Joel on Software,中文《软件随想录》 以下是原文的摘取,发表于2002年,要二十年了。
TCP协议的神奇之处,计算机科学家们通常会将其称作为“抽象”:将复杂的问题用简单的方式表现出来。事实上,很多计算机编程工作都是在进行抽象。字符串库做了什么? 它能让我们觉得计算机可以像处理数字那样处理文字。 文件系统是什么?它让硬盘不再是一组高速旋转的磁性盘块,而是一个有着目录层级结构、能够按字节存储字符信息的设备。
我们继续说TCP。刚才我打了一个比方,有些人可能觉得那很疯狂。但是,当我说TCP协议可以保证消息一定能够到达,事实上并非如此。如果你的宠物蛇把网线给咬坏了,那即便是TCP协议也无法传输数据; 如果你和网络管理员闹了矛盾,他将你的网口接到了一台负载很高的交换机上,那即便你的数据包可以传输,速度也会奇慢无比。
这就是我所说的“抽象泄漏”。TCP协议试图提供一个完整的抽象,将底层不可靠的数据传输包装起来,但是,底层的传输有时也会发生问题,即便是TCP协议也无法解决,这时你会发现,它也不是万能的。 TCP协议就是“抽象泄漏定律”的示例之一,其实,几乎所有的抽象都是泄漏的。这种泄漏有时很小,有时会很严重。下面再举一些例子:
-
对于一个简单的操作,如循环遍历一个二维数组,当遍历的方式不同(横向或纵向),也会对性能造成很大影响,这主要取决于数组中数据的分布——按某个方向遍历时可能会产生更多的页缺失(page fault),而页缺失往往是非常消耗性能的。 即使是汇编程序员,他们在编写代码时也会假设程序的内存空间是连续的,这是系统底层的虚拟内存机制提供的抽象,而这一机制在遇到页缺失时就会消耗更多时间。
-
SQL语言意图将过程式的数据库访问操作封装起来,你只需要告诉操作系统你想要的数据,系统会自动生成各个步骤并加以执行。但在有些情况下,某些SQL查询会比其逻辑等同的查询语句要慢得多。 一个著名的示例是,对大多数SQL服务器,指定“WHERE a = b AND b = c AND a = c”要比单纯指定“WHERE a = b AND b = c”快的多,即便它们的结果集是一致的。在使用SQL时,我们不需要思考过程,只需关注定义。 但有时,这种抽象会造成性能上的大幅下降,你需要去了解SQL语法分析器的工作原理,找出问题的原因,并想出应对措施,让自己的查询运行得更快。
-
即便有NFS、SMB这样的协议可以让你像在处理本地文件一样处理远程文件,如果网络传输很慢,或是完全中断了,程序员就需要手动处理这种情况。所以,这种“远程文件即本地文件”的抽象机制是存在泄漏的。 这里举一个现实的例子:如果你将用户的home目录加载到NFS上(一次抽象),你的用户创建了.forward文件,用来转发他所有的电子邮件(二次抽象),当NFS服务器宕机,.forward文件会找不到,这样就无法转发邮件了,造成丢失。
-
C++的字符串处理类库相当于增加了一种基础数据类型:字符串,将各种操作细节封装起来,让程序员可以方便地使用它。几乎所有的C++字符串类都会重载+操作符,这样你就能用 s + “bar” 来拼接字符串了。 但是,无论哪种类库都无法实现 “foo” + “bar” 这种语句,因为在C++中,字符串字面量(string literal)都是char *类型的。这就是一种泄漏。 (有趣的是,C++语言的发展历程很大一部分是在争论字符串是否应该在语言层面支持。我个人并不太能理解这为何需要争论。)
-
当你在雨天开车,虽然你坐在车里,前窗有雨刷,车内有空调,这些措施将“天气”给抽象走了。但是,你还是要小心雨天的轮胎打滑,有时这雨下得太大,可见度很糟,所以你还是得慢行。 也就是说,“天气”因素并没有被完全抽象走,它也是存在泄漏的。
抽象泄漏引发的麻烦之一是,它并没有完全简化我们的工作。当我指导别人学习C++时,我当然希望可以跳过char *和指针运算,直接讲解STL字符串类库的使用。 但是,当某一天他写出了 “foo” + “bar” 这样的代码,并询问我为什么编译错误时,我还是需要告诉它char *的存在。或者说,当他需要调用一个Windows API, 需要指定OUT LPTSTR参数,这时他就必须学习char *、指针、Unicode、wchar_t、TCHAR头文件等一系列知识,这些都是抽象泄漏。
在指导COM编程时,我希望可以直接让大家如何使用Visual Studio的代码生成向导。但将来如果出现问题,学员面对这些生成的代码会不知所从,这时还是要回过头来学习IUnknown、CLSID、ProgIDS等等。天呐!
在指导ASP.NET编程时,我希望可以直接告诉大家双击页面上的控件,在弹出的代码框中输入点击响应事件。的确,ASP.NET将处理点击的HTML代码抽象掉了,但问题在于, ASP.NET的设计者需要动用JS来模拟表单的提交,因为HTML中的标签是没有这一功能的。这样一来,如果终端用户将JS禁止了,这个程序将无法运行。初学者会不知所措,直至他了解ASP.NET的运作方式, 了解它究竟将什么样的工作封装起来了,才能进一步排查。
由于抽象定律的存在,每当有人说自己发现了一款新的代码生成工具,能够大大提高我们的编程效率时,你会听很多人说“先学习手工编写,再去用工具生成”。代码生成工具是一种抽象, 同样也会泄漏,唯一的解决方法是学习它的实现原理,即它抽象了什么。所以说抽象只是用于提高我们的工作效率的,而不会节省我们的学习时间。
这就形成了一个悖论:当我们拥有越来越高级的开发工具,越来越好的“抽象”,要成为一个高水平的程序员反而越来越困难了。
我在微软实习的第一年,是为Macintosh编写字符串处理类库。很普通的一个任务:编写 strcat 函数,返回一个指针,指向新字符串的尾部。几行C语言代码就能实现了,这些都是从K&R这本C语言编程书上学习到的。
如今,我在CityDesk供职,需要使用Visual Basic、COM、ATL、C++、InnoSetup、Internet Explorer原理、正则表达式、DOM、HTML、CSS、XML等等,这些相对于古老的K&R来说都是非常高级的工具, 但是我仍然需要用到K&R的相关知识,否则会困难重重。
十年前,我们会想象未来能够出现各种新式的编程范型,简化我们的工作。
的确,这些年我们创造的各类抽象使得开发复杂的大型软件变得比十五年前要简单得多,就像GUI和网络编程。现代的面向对象编程语言让我们的工作变得高效快速。
但突然有一天,这种抽象泄漏出一个问题,解决它需要耗费两星期。
如果你需要招录一个VB程序员,那不是一个好主意,因为当他碰到VB语言泄漏的问题时,他会变得寸步难行。
抽象泄漏定律正在阻碍我们前进。