为什么任何系统都会存在Bug?什么是抽象漏洞定律?
1 【引子】
我们经常会讨论一个话题,有没有一种系统是没有Bug的?我们可以闭上眼睛想一想,好像没有。就目前我们所知的系统来说,没有不存在Bug的。这个问题延伸一下就是:为什么任何系统都会存在Bug呢?
我们开发一套系统的正确姿势往往是找出问题的本质,去掉看似无关紧要的因素,抓住主要矛盾,这就是我们常说的抽象的过程。
随着系统开发的不断深入,系统变得越来越复杂,有些以前忽略掉的因素在某些情况下逐步变成了必要因素。
随着系统变得越来越复杂,软件开发人员不得不依赖更多的抽象。而抽象的本质就是化繁为简,隐藏事物的复杂性。
这里就存在一个矛盾,就是系统不断增加的复杂性与试图简化这个复杂性的抽象无法完美匹配的矛盾。
抽象与具体事物的匹配缺失为Bug的产生提供了天然的条件。
这就是抽象漏洞定律。
通过本文,我们一起来探索一下这个问题。
2 【抽象漏洞定律】
抽象漏洞定律的提出者是乔尔-斯波尔斯基(Joel Spolsky),即指所有的抽象在某种程度上都是有漏洞的。
3 【抽象漏洞例子】
3.1 【网络对象与文件对象】
将网络对象抽象为一个文件系统对象。这让处理变得简单,因为大家都知道如何读写文件。不幸的是,所有的网络问题都会被"漏掉":
ü 延迟很大,而且不可预知;
ü 有时数据丢失,你不得不重新打开;
ü 等等。
这种情况叫做抽象不足。
注意,相反的:将文件对象抽象为一个远程网络资源对象。在这种情况下,调用者是要考虑处理延迟和超时问题的,但是系统的复杂度增加了。
这种情况是抽象过度。
3.2 【SQL通配符查询】
下面来看一个简单的SQL查询。
如果我们用:
SELECT id, first_name, last_name, age, subject FROM student_details;
而不是:
SELECT * FROM student_details;
虽然这个请求在逻辑上是等价的,但如果指定各个列名,性能会更好。通配符抽象没有改变逻辑上数据返回结果,但会带来性能上的开销。
3.3 【SQL数据通过索引查询】
SQL通过创建索引抽象大大提高了数据访问速度。但是由于索引是建立在一定的算法抽象之上的,比如特定的几个字段。因此在进行数据搜索的时候,看上去多个相似的查询,查询的速度迥然不同,这是因为有的查询使用了索引,而另一个没有。
3.4 【垃圾收集】
垃圾收集将有限的内存分配器抽象成一个无限的内存系统。有了垃圾回收机制,理论上你就不用担心内存问题了。
真的是这样吗?
很不幸,在某些条件下,比如当你的内存分配率达到或超过50%时,垃圾收集过程会启动,这会带来性能上的代价。
3.5 【分支预测】
有人做过一个实验,他有一个循环,如果值<128时,打印值;如果不<128,也打印值。通过事先对数组进行排序,他得到了6倍的性能提升,因为处理器能够预测到"下一个值可能会和上一个值的情况一样"。当看到代码评估速度几乎快了一个数量级,没有明显的原因,这肯定是抽象漏洞在作怪。
3.6 【亚马逊购物】
当你在亚马逊网站上购物时,你会看到类似“明天送达”这样的选项。对应这样的一个功能,如果在货物运输途中遭遇了飓风,导致运送终止,这算不算是一种抽象漏洞?
当然算。
3.7 【哈希表】
以哈希为例,从明文数据根据哈希函数计算出一个哈希值。我们程序员很少关心如何实现哈希函数,因为很多程序库都提供了相关的接口,比如映射(Map)数据结构就是使用哈希的典型例子。对我们调用者来说,哈希部分的实现是个黑盒子。通过调用一些抽象程序接口,我们用到了哈希作为一个抽象的功能。
这个抽象的一个漏洞就是不同的数据可能会产生相同的哈希值。
3.8 【Axios程序库】
Axios程序库是一个处理HTTP请求的程序库,它将浏览器中的JavaScript fetch API包裹起来。当出现HTTP错误时,Axios会将其转化为JavaScript错误,这些错误中会包含程序包自定义的数据结构。Axios的这种处理是假定大多数用例不需要知道原生的HTTP错误。但是有些应用可能不需要这种封装。
4 【抽象漏洞的种类】
上面我们看了一些例子,大家对于抽象漏洞有了比较直观的认识,现在我们来看看抽象漏洞的种类。
4.1 【性能】
在一些抽象漏洞的例子中,我们会发现性能受到了影响。一条SQL查询可能比预期的运行慢很多。一个数组访问可能会比预期的时间长很多。
上面分支预测,SQL通配符查询和索引查询都属于这类例子。
4.2 【实际与预期不符】
在一些抽象漏洞的例子中,我们会发现实际的行为与我们抽象的预期不相符。比如上面的AXIOS例子,一个HTTP 状态码被转化成JavaScript程序库的错误信息。
4.3 【时空耦合漏洞】
另外一种抽象漏洞是时空耦合漏洞。简单说就是“做了你能做的,但就是不工作”。
比如说,一个接口只有当各个方法按照一定的顺序调用时才会正常工作。
这种漏洞要求开发人员学习一些外围的知识,这些可能与当前的业务逻辑无关。
上面的网络对象与文件对象,亚马逊购物都属于这类例子。
5 【抽象漏洞思考的发展轨迹】
5.1 1974
在设计高级程序设计语言时,抽象漏洞存在的事实是公认的,因为高级程序设计语言倾向于把低层细节抽象化。这里引用了Niklaus Wirth的一句话,很有借鉴意义:
· 我发现大量程序的表现很差,因为语言倾向于隐藏 "发生了什么事",而说的好听点叫做"不要让细节干扰程序员。"
5.2 1980
在程序设计语言的背景下,语言设计者所做的一些决定被认为是有主观臆断的,也就是说,这些决定约束了开发人员以特定的方式使用语言。例如,一个开发者需要两个三角形数组,但被迫使用两个矩形数组(内存较多),或者将它们打包成一个矩形数组(代码复杂)。可以说,语言提供的抽象不适合这种特殊的用例。
5.3 1992
Gregor Kiczales在一次研讨会上解释了抽象漏洞。他提出将抽象分为两部分:一部分以传统的方式进行抽象,另一部分允许客户端对实现进行一些控制。他将其称为由元级架构和元对象协议支持的开放实现。对于设计元级接口,他指出了四个设计原则:范围控制、概念分离、增量性和健壮性。
5.4 2002
Joel Spolsky在他的博客Joelon Software coins上,解释了 "抽象漏洞定律"。
5.5 2006
微软的Ryan Bemrose指出了 "抽象漏洞定律"的一个推理,"抽象不应该隐藏或禁用它所抽象的东西"。他举了一个可以与第三方插件交互的IRCbot的例子。IRC协议本身就是通过面向对象的接口抽象出来的。人们发现,使用自定义模式的插件无法正常运行,因为这样的功能被抽象禁用了。抽象是有用的,但我们不应该试图抽象掉所有的东西。
5.6 2017
在ContainerWorld 2017年的大会上,一位演讲者指出,容器也是有漏洞的抽象。容器内运行的进程要依赖于I/O性能、版本、配置、旧镜像的垃圾回收等。两个争夺相同I/O的容器会相互受到影响。
6 【小结】
我们在本文中通过对抽象漏洞定律的概念,例子,种类和发展轨迹等几个方面学习和探讨了这一现象。正如不存在绝对真理一样,彻底的杜绝抽象漏洞是不可能的,但我们可以像定义相对真理一样,在我们可认知的范围内确保当前的抽象无漏洞。
欢迎讨论。
- 点赞
- 收藏
- 关注作者
评论(0)